From 2d3a970da71512cfffc73f299bd771ddf5a09eb7 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:51:11 +0200 Subject: [PATCH 001/353] fix(shader): keep each BDA page-table walk to one inlined copy per read RADV inlines every SPIR-V function, DontInline included, so every get_bda_pointer call site becomes its own copy of the page-table search, and NIR's copy propagation of variables grows with each copy. The lookup now keeps its search bounds in phis and records its header and range faults from one call site each, picking the first failing check; a byte read and the byte fallback of a span read loop over one lookup instead of unrolling one per byte or dword. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 11 ++- .../include/SpirvBackend/SpirvBda.hpp | 1 + .../SpirvBackend/src/SpirvBdaFault.cpp | 5 + .../SpirvBackend/src/SpirvBdaLookup.cpp | 81 ++++++++------- .../SpirvBackend/src/SpirvBdaRead.cpp | 99 ++++++++++++++----- 5 files changed, 137 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f7..fab9d851c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2481,6 +2481,9 @@ void verifyBdaReadFallbackFunctions() { std::size_t mainProbes = 0; std::size_t spanCalls = 0; std::array readerLoads{}; + std::size_t lookupFaults = 0; + std::size_t lookupVariables = 0; + std::size_t readerLookups = 0; for (std::size_t cursor = 5; cursor < five.size();) { const auto length = five[cursor] >> 16u; require(length != 0 && length <= five.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); @@ -2501,13 +2504,19 @@ void verifyBdaReadFallbackFunctions() { if (callee == span) ++spanCalls; } if (op == spv::OpLoad && function == reader) ++readerLoads[length > 4u && (five[cursor + 4] & spv::MemoryAccessVolatileMask) != 0u]; + if (op == spv::OpFunctionCall && function == "get_bda_pointer" && names[five[cursor + 3]] == "record_bda_fault") ++lookupFaults; + if (op == spv::OpVariable && function == "get_bda_pointer") ++lookupVariables; + if (op == spv::OpFunctionCall && function == reader && names[five[cursor + 3]] == "get_bda_pointer") ++readerLookups; cursor += length; } require(definitions["record_bda_fault"] == 1u && definitions[reader] == 1u && definitions[span] == 1u, "BDA read functions: the fault, byte read and span read functions are not defined"); for (const auto& [name, count] : definitions) require(count == 1u, "BDA read functions: a function is defined twice"); require(compareExchanges == 1u, "BDA read functions: a fault is recorded outside record_bda_fault"); require(mainLookups == 0u, "BDA read functions: a read site looks up its bytes inline"); - require(readerLoads[coherent] == 4u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(readerLoads[coherent] == 1u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(lookupFaults <= 4u, "BDA read functions: the page-table lookup records its faults from more than 4 call sites"); + require(lookupVariables == 0u, "BDA read functions: the page-table lookup keeps its search bounds in function variables"); + require(readerLookups == 1u, "BDA read functions: the byte read does not look up its bytes once, in a loop"); require(mainProbes == 0u, "BDA read functions: a read site probes or reads bytes outside its span read function"); require(spanCalls == 5u, "BDA read functions: a read site does not call its span read function once"); require(five.size() - one.size() < 4u * 150u, "BDA read functions: a read site takes 150 SPIR-V words or more"); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 0b308f556c..21dda89e68 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -17,6 +17,7 @@ void DefineBdaFaultFunction(SpirvEmitterState& state); void DefineBdaDwordReadFunctions(SpirvEmitterState& state); void DefineBdaSpanReadFunctions(SpirvEmitterState& state); void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason); void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index ca8c1a2678..4b0d60d921 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -75,6 +75,11 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast(reason)), address, bytes, instruction); } +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason) { + if (state.bdaFaultFunction == 0 || !state.program.Info().usesDma) throw std::runtime_error("BDA fault function is missing"); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, reason, address, bytes, instruction); +} + void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { if (state.program.Info().usesDma) { const auto u64 = TypeScalarU64(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index 248ff205c0..b48e8775b5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -2,6 +2,9 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include +#include +#include +#include namespace ShaderRecompiler { @@ -24,11 +27,6 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto boolean = TypeBool(state); const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; 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 load = [&](std::uint32_t pointer) { - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, value, pointer); - return value; - }; 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)); @@ -39,11 +37,6 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); EmitLabel(state, state.module.AllocateId()); - const auto low = state.module.AllocateId(); - const auto high = state.module.AllocateId(); - const auto pointer = TypePointer(state, spv::StorageClassFunction, u32); - state.module.AddFunction(spv::OpVariable, pointer, low, spv::StorageClassFunction); - state.module.AddFunction(spv::OpVariable, pointer, high, spv::StorageClassFunction); const auto fail = [&](std::uint32_t condition, BdaAbi::FaultReason reason) { if (recordFaults) { ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); @@ -51,29 +44,53 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r ReturnBdaZeroIf(state, condition); } }; + const auto failFirst = [&](std::initializer_list> checks) { + auto reason = constant(0u); + for (auto check = std::rbegin(checks); check != std::rend(checks); ++check) { + reason = Select(state, u32, check->first, constant(static_cast(check->second)), reason); + } + const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); + if (!recordFaults) { + ReturnBdaZeroIf(state, failed); + return; + } + const auto failedLabel = state.module.AllocateId(); + const auto next = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, failed, failedLabel, next); + EmitLabel(state, failedLabel); + RecordBdaFaultReason(state, address, bytes, instruction, reason); + state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); + EmitLabel(state, next); + }; const auto length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, u32, length, state.bdaPagetableVariable, 0u); fail(binary(spv::OpULessThan, boolean, length, constant(4)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable); const auto count = BdaLoadWord(state, constant(1)); const auto available = binary(spv::OpISub, u32, length, constant(4)); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow); const auto end = binary(spv::OpIAdd, u64, address, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow); - state.module.AddFunction(spv::OpStore, low, constant(0)); - state.module.AddFunction(spv::OpStore, high, count); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow}, + }); const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); const auto continuation = state.module.AllocateId(); const auto merge = state.module.AllocateId(); + const auto entryLabel = state.currentLabel; state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); - const auto lower = load(low); - const auto upper = load(high); + const auto lower = state.module.AllocateId(); + const auto upper = state.module.AllocateId(); + const auto newLow = state.module.AllocateId(); + const auto newHigh = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, lower, constant(0), entryLabel, newLow, continuation); + state.module.AddFunction(spv::OpPhi, u32, upper, count, entryLabel, newHigh, continuation); const auto search = binary(spv::OpULessThan, boolean, lower, upper); state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, search, body, merge); @@ -81,17 +98,13 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto midpoint = binary(spv::OpIAdd, u32, lower, binary(spv::OpShiftRightLogical, u32, binary(spv::OpISub, u32, upper, lower), constant(1))); const auto entry = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, midpoint, constant(8))); const auto before = binary(spv::OpULessThan, boolean, address, BdaLoadAddress(state, entry)); - const auto newLow = state.module.AllocateId(); - const auto newHigh = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, u32, newLow, before, lower, binary(spv::OpIAdd, u32, midpoint, constant(1))); state.module.AddFunction(spv::OpSelect, u32, newHigh, before, midpoint, upper); - state.module.AddFunction(spv::OpStore, low, newLow); - state.module.AddFunction(spv::OpStore, high, newHigh); state.module.AddFunction(spv::OpBranch, continuation); EmitLabel(state, continuation); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - const auto index = load(low); + const auto index = lower; fail(binary(spv::OpIEqual, boolean, index, constant(0)), BdaAbi::FaultReason::Unmapped); const auto selected = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, binary(spv::OpISub, u32, index, constant(1)), constant(8))); const auto at = [&](std::uint32_t offset) { return binary(spv::OpIAdd, u32, selected, constant(offset)); }; @@ -99,16 +112,18 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto finish = BdaLoadAddress(state, at(2)); const auto base = BdaLoadAddress(state, at(4)); const auto permissions = BdaLoadWord(state, at(6)); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission); - fail(binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable); const auto offset = binary(spv::OpISub, u64, address, begin); const auto result = binary(spv::OpIAdd, u64, base, offset); - fail(binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow); const auto deviceEnd = binary(spv::OpIAdd, u64, result, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission}, + {binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, + }); state.module.AddFunction(spv::OpReturnValue, result); 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 f07e159eef..a3d8ee606a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -66,16 +66,41 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui return value; } -std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { - auto result = ConstantU32(state, 0u); - for (std::uint32_t byte = 0; byte < bytes; ++byte) { - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto value = EmitBdaByte(state, guest, instruction, accessMask); - result = Binary(state, spv::OpBitwiseOr, TypeU32(state), result, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), value, ConstantU32(state, byte * 8u))); - } +std::uint32_t EmitBdaByteLoop(SpirvEmitterState& state, std::uint32_t address, std::uint32_t count, std::uint32_t instruction, std::uint32_t accessMask) { + const auto u32 = TypeU32(state); + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto byte = state.module.AllocateId(); + const auto result = state.module.AllocateId(); + const auto nextByte = state.module.AllocateId(); + const auto nextResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, byte, ConstantU32(state, 0u), entry, nextByte, continuation); + state.module.AddFunction(spv::OpPhi, u32, result, ConstantU32(state, 0u), entry, nextResult, continuation); + const auto more = Binary(state, spv::OpULessThan, TypeBool(state), byte, count); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + const auto value = EmitBdaByte(state, guest, instruction, accessMask); + state.module.AddFunction(spv::OpBitwiseOr, u32, nextResult, result, Binary(state, spv::OpShiftLeftLogical, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, byte, ConstantU32(state, 3u)))); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextByte, byte, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); return result; } +std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { + if (bytes == 1u) return EmitBdaByte(state, address, instruction, accessMask); + 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. @@ -180,26 +205,48 @@ std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent) } return composite(loaded); }, [&] { - std::array bytes{}; - for (std::uint32_t dword = 0; dword < 4u; ++dword) { - bytes[dword] = EmitValueOrZeroIfCondition(state, isUsed(dword), [&] { - const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, ConstantU32(state, dword)); - const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { - return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, ConstantU32(state, dword * 4u)), instruction); - }); - const auto pair = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); - if (stops) { - const auto stopped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); - StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); - } - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, value, pair, 0u); - return value; + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto dword = state.module.AllocateId(); + const auto gathered = state.module.AllocateId(); + const auto nextDword = state.module.AllocateId(); + const auto nextGathered = state.module.AllocateId(); + const auto zeros = state.module.Constant(spv::OpConstantComposite, values, zero, zero, zero, zero); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, dword, zero, entry, nextDword, continuation); + state.module.AddFunction(spv::OpPhi, values, gathered, zeros, entry, nextGathered, continuation); + const auto more = Binary(state, spv::OpULessThan, boolean, dword, ConstantU32(state, 4u)); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto used32 = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, used, Binary(state, spv::OpShiftLeftLogical, u32, ConstantU32(state, 1u), dword)), zero); + const auto value = EmitValueOrZeroIfCondition(state, used32, [&] { + const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, dword); + const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { + return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, Binary(state, spv::OpShiftLeftLogical, u32, dword, ConstantU32(state, 2u))), instruction); }); - } - return composite(bytes); + const auto pair = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); + if (stops) { + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); + } + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, loaded, pair, 0u); + return loaded; + }); + state.module.AddFunction(spv::OpVectorInsertDynamic, values, nextGathered, gathered, value, dword); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextDword, dword, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); + return gathered; }); auto returned = read; if (stops) { From 3bc3e35a0282ec46e0f52877e6e0d8ecf035411e Mon Sep 17 00:00:00 2001 From: marcio-ch Date: Mon, 12 Oct 2026 02:27:16 -0300 Subject: [PATCH 002/353] fix(agc): handle the fill kernel that reads its pattern from a buffer The 16-byte fill kernel has a variant that loads its pattern with s_buffer_load_dwordx4 through the V# in s[4:7] instead of taking it from the user SGPRs. Vampire Crawlers (PPSA30703) clears its UI canvas HTILE with it every frame; run as a shader, the depth surface never cleared and every UI draw failed its GEQUAL depth test, leaving the screen black. --- .../src/Driver/Memory/BufferFill.cpp | 22 +++++++++++-- .../tests/execution/DepthFastClearSubmit.cpp | 33 +++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) 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 b8e7adb777..de986fe6cc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -14,13 +14,22 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool matchesBufferPatternFillKernel(std::span code) { + static constexpr std::array kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin()); +} + +} + bool Driver::matchesFillKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr; if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false; if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true; static constexpr std::array fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; - if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false; + if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; @@ -165,7 +174,16 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(static_cast(groups[0]) * 64u, numRecords); base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); bytes = static_cast(records * 16u); - pattern = {userData[4], userData[5], userData[6], userData[7]}; + if (matchesBufferPatternFillKernel(code)) { + const auto source = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(source), sizeof(pattern))) return false; + GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4); + } else { + pattern = {userData[4], userData[5], userData[6], userData[7]}; + } } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static std::atomic fills{0}, filledBytes{0}, cpuFills{0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp index 6f43c74925..0ecf904715 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -88,9 +88,12 @@ void DmaFill(const GuestBlock& htile, std::uint32_t bytes, std::uint32_t pattern constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; -void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t bytes) { +constexpr std::array BufferPatternFillKernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t bytes, const GuestBlock* patternBuffer = nullptr) { const auto kernel = code.Address(); const auto base = htile.Address(); + const auto source = patternBuffer != nullptr ? patternBuffer->Address() : 0; Submit({ Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), @@ -102,10 +105,10 @@ void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), Pm4Packet(0x76, {0x242, bytes / 16u}), Pm4Packet(0x76, {0x243, 0x4bfacu}), - Pm4Packet(0x76, {0x244, 0}), - Pm4Packet(0x76, {0x245, 0}), - Pm4Packet(0x76, {0x246, 0}), - Pm4Packet(0x76, {0x247, 0}), + Pm4Packet(0x76, {0x244, Low(source)}), + Pm4Packet(0x76, {0x245, source != 0 ? (High(source) & 0xffffu) | (16u << 16u) : 0}), + Pm4Packet(0x76, {0x246, source != 0 ? 1u : 0}), + Pm4Packet(0x76, {0x247, source != 0 ? 0x4dfacu : 0}), Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), }); } @@ -144,6 +147,21 @@ void DispatchFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& sha Require(Draw(device, shaders, surface, {}) == 0u, "the depth written after the compute fill clear was lost"); } +void DispatchBufferPatternFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), BufferPatternFillKernel.data(), sizeof(BufferPatternFillKernel)); + auto& pattern = NewBlock(); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + std::memset(pattern.Data(), 0xff, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {}) == 0u, "a compute fill with the buffer pattern 0xffffffff (ZMask 0xf) cleared the depth"); + std::memset(pattern.Data(), 0, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill with the buffer pattern 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); +} + void DepthOnlyFillKeepsStencil(AgcDriver::VulkanDevice& device, const Shaders& shaders) { const auto& htile = NewBlock(); const Surface surface{NewBlock().Address(), NewBlock().Address(), htile.Address(), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; @@ -168,7 +186,10 @@ int main() { DmaFillClearsDepth(*device, shaders); DepthOnlyFillKeepsStencil(*device, shaders); const bool computeRan = StoresFills(*device, NewBlock()); - if (computeRan) DispatchFillClearsDepth(*device, shaders); + if (computeRan) { + DispatchFillClearsDepth(*device, shaders); + DispatchBufferPatternFillClearsDepth(*device, shaders); + } else std::printf("compute fill not run: the device does not store fills into imported guest memory\n"); LibcRunShutdown_nid_postfix(); std::puts(computeRan ? "depth fast clear submit tests passed" : "depth fast clear submit tests passed without the compute fill"); From ff3eae6c9ac3d1e1bd24057a27b8c980da43401e Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Mon, 12 Oct 2026 15:06:04 +0200 Subject: [PATCH 003/353] feat(agc): sample non-sRGB textures through FORCE_DEGAMMA samplers as sRGB views A sampler with FORCE_DEGAMMA decodes the texels of a non-sRGB 8-bit texture as sRGB. Such a binding was refused; it now samples an sRGB view of the same image. Snapshot images whose format has an sRGB twin (R8, R8G8, RGBA8, BGRA8, ABGR8 packed, BC1/2/3/7 UNORM) are created with MUTABLE_FORMAT, and storage images are already mutable. The view is made on first use for the full, first-layer or base-level range, with sampled usage only and the descriptor's minimum LOD. A format without an sRGB twin is still refused. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/Sampler.hpp | 2 +- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 8 +++ .../libSceAgcDriver/Graphics/src/Sampler.cpp | 6 +- .../Graphics/src/ShaderResources.cpp | 6 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 62 ++++++++++++++++++- .../execution/ImageSampleForceDegamma.cpp | 17 +++-- 7 files changed, 92 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 76804ccfc3..d7749e2da3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -66,7 +66,7 @@ private: }; void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); -void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); +bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers); void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b29f6df0c..ce92808548 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -464,6 +464,7 @@ private: std::vector> textures; std::vector textureFirstLayer; std::vector textureBaseLevel; + std::vector textureSrgb; std::vector> storageTextures; std::vector storageMips; std::vector storageKeys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index a01cd7efc7..639bff720d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -83,6 +83,8 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } VkImageView BaseLevelView() const; + bool HasSrgbView() const { return srgbInfo.has_value(); } + VkImageView SrgbView(bool firstLayer, bool baseLevel) const; struct ViewRange { VkImageViewType type; std::uint32_t levels; @@ -106,6 +108,7 @@ private: void release() noexcept; void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); + void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); // Held by value: cached textures outlive the Context of the draw that created them. Context context; @@ -120,6 +123,10 @@ private: std::optional baseLevelInfo; mutable std::mutex baseLevelMutex; mutable VkImageView baseLevelView = VK_NULL_HANDLE; + std::optional srgbInfo; + float srgbMinLod = 0.0f; + mutable std::mutex srgbMutex; + mutable std::array srgbViews{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; VkDeviceSize countedBytes = 0; @@ -135,6 +142,7 @@ private: }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); +VkFormat SrgbTwinFormat(VkFormat format); // A guest texture a shader writes through a storage image. It is uploaded like a sampled texture; // after the GPU work completes its results are stored to guest memory (retiled, changed bytes only), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 3bf2f95565..214fcaf05f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -113,10 +113,12 @@ namespace AgcDriver::Graphics { return requiresNonSeamlessCube; } - void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + if (IsSrgbTextureFormat(guestFormat)) return false; for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { - if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented"); + if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; } + return false; } void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 63bd7b18ec..f7b0bfde00 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1619,7 +1619,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); - else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); + else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; @@ -3281,7 +3281,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind continue; } const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); - RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers); + const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); @@ -3306,6 +3306,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers); + if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented"); if (!shaderSamplers.empty()) { const bool singleLevel = DescriptorSingleLevel(words); const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); @@ -3323,6 +3324,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); textureBaseLevel.push_back(baseLevel); + textureSrgb.push_back(forcedSrgb); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(textures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index af6d40a31d..a319e3d8c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -267,7 +267,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto linearBytes = sliceLinearBytes * arrayLayers; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); imageInfo.imageType = ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; @@ -446,6 +446,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -648,6 +649,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -695,6 +697,44 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma baseLevelInfo = viewInfo; } +void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + const auto format = SrgbTwinFormat(viewInfo.format); + if (format == VK_FORMAT_UNDEFINED) return; + viewInfo.pNext = nullptr; + viewInfo.format = format; + srgbInfo = viewInfo; + srgbMinLod = EffectiveMinLod(descriptor); +} + +VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { + Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding"); + std::lock_guard lock(srgbMutex); + auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u]; + if (slot != VK_NULL_HANDLE) return slot; + VkImageViewCreateInfo info = *srgbInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + if (baseLevel) { + info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.levelCount = 1; + info.subresourceRange.layerCount = 1; + } else { + if (firstLayer) { + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.layerCount = 1; + } + if (srgbMinLod != 0.0f) { + minLod.minLod = srgbMinLod; + minLod.pNext = info.pNext; + info.pNext = &minLod; + } + } + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB"); + return slot; +} + VkImageView Texture::BaseLevelView() const { Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); std::lock_guard lock(baseLevelMutex); @@ -710,6 +750,10 @@ void Texture::release() noexcept { firstLayerView = VK_NULL_HANDLE; if (baseLevelView) context.Function("vkDestroyImageView")(context.device, baseLevelView, nullptr); baseLevelView = VK_NULL_HANDLE; + for (auto& srgbView : srgbViews) { + if (srgbView) context.Function("vkDestroyImageView")(context.device, srgbView, nullptr); + srgbView = VK_NULL_HANDLE; + } // The image and its memory go with the last holder: this texture, or the batch still uploading it. owned.reset(); image = VK_NULL_HANDLE; @@ -844,6 +888,22 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear); } +VkFormat SrgbTwinFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UNORM: return VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK; + case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK; + case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK; + default: return VK_FORMAT_UNDEFINED; + } +} + VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED; struct Table { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index e106d7cfab..bab71fa850 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -24,6 +24,7 @@ constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Size = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8888Srgb = 130; +constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -126,7 +127,7 @@ void RequireSame(const std::array& forced, const std::array< } } -void RequireDecoded(const std::array& sampled) { +void RequireDecoded(const std::array& sampled, const char* name) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto x = tid % Size; const auto y = (tid / Size) % Size; @@ -134,7 +135,7 @@ void RequireDecoded(const std::array& sampled) { const auto code = Code8(x, y, channel); const float expected = channel == 3u ? static_cast(code) / 255.0f : SrgbToLinear(code); const float value = sampled[tid * 4u + channel]; - Require(std::fabs(value - expected) <= 0.005f, "8_8_8_8_SRGB through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); + Require(std::fabs(value - expected) <= 0.005f, std::string(name) + " through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); } } } @@ -158,13 +159,21 @@ int main() { FillTexels(); FillInput(true); const auto forcedPoint = Sample(*device, Format8888Srgb, FilterPoint, ForceDegamma); - RequireDecoded(forcedPoint); + RequireDecoded(forcedPoint, "8_8_8_8_SRGB"); RequireSame(forcedPoint, Sample(*device, Format8888Srgb, FilterPoint, 0u), "8_8_8_8_SRGB, point"); FillInput(false); const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma); RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear"); RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly"); - Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); + FillInput(true); + const auto unormPoint = Sample(*device, Format8888UNorm, FilterPoint, ForceDegamma); + RequireDecoded(unormPoint, "8_8_8_8_UNORM"); + RequireSame(unormPoint, forcedPoint, "8_8_8_8_UNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + const auto unormPlain = Sample(*device, Format8888UNorm, FilterPoint, 0u); + Require(unormPlain[0] != unormPoint[0] || unormPlain[4] != unormPoint[4], "8_8_8_8_UNORM without FORCE_DEGAMMA sampled the decoded values"); + FillInput(false); + RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + Reject(*device, Format32Float, "guest format 22, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { From 69d0dc228809a6786c99a38bb69171e830f61461 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:40:09 -0400 Subject: [PATCH 004/353] perf(shader): treat f32 min, max, med3, floor and ceil results as denormal-free In the float modes that flush f32 denormals, writeOperand flushes the result of v_min/max/min3/max3/med3_f32 and v_floor/ceil_f32. DenormalFlushEliminator drops a flush only when it can show the value isn't a denormal, and it had no rule for these opcodes, so their result flush stayed even when every operand had already been flushed. FPMin32, FPMax32, FPMinTri32, FPMaxTri32 and FPMedTri32 return one of their operands' bits, or the and or or of two zeros, so the result is denormal-free when every operand is. FPFloor32 and FPCeil32 return an integer, a zero, an infinity or a NaN whatever their operand. In IEEE mode the min and max results and the floor and ceil results go through selects with a quieted operand, which the eliminator can't follow, so their flush stays there. agc_denormal_flush_elimination covers the five min and max forms with flushed and with unknown operands, floor and ceil of an unknown value, and a max carried around a loop. --- .../tests/DenormalFlushElimination.cpp | 60 +++++++++++++++++++ .../src/DenormalFlushEliminator.cpp | 7 +++ 2 files changed, 67 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index 47020acb0a..3db6a8ce08 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -150,6 +150,66 @@ int main() { return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses(); }); + passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto flushedResult = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)})); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& z = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = { + &flushedResult(IrOpcode::FPMin32, {&x, &y}), + &flushedResult(IrOpcode::FPMax32, {&y, &x}), + &flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}), + &flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}), + &flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}), + }; + auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw}); + auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw}); + for (auto* flushed : removed) b.keep(*flushed); + b.keep(mixedPair); + b.keep(mixedMedian); + const bool counted = b.eliminate() == 5u; + bool kept = false; + for (auto* flushed : removed) kept |= flushed->HasUses(); + return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses(); + }); + + passed &= run("a floor or ceil needs no flush, even of an unknown value", [] { + Builder b; + auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})})); + auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})})); + b.keep(floored); + b.keep(ceiled); + return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses(); + }); + + passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + head.AddBranch(&body); + body.AddBranch(&body); + body.AddBranch(&exit); + auto& initial = b.flush(b.unknown()); + b.block = &body; + auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + body.AppendInstruction(&carried); + auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())}); + auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})}); + carried.AddPhiOperand(&head, &initial); + carried.AddPhiOperand(&body, &maximum); + auto& flushed = b.flush(maximum); + b.block = &exit; + b.keep(flushed); + return b.eliminate() == 0u && flushed.HasUses(); + }); + passed &= run("a phi of flushed values needs no flush", [] { Builder b; IrBlock& head = *b.block; diff --git a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp index 7de56772ea..d448da5e6a 100644 --- a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -51,6 +51,8 @@ private: case IrOpcode::ConvertF32S32: case IrOpcode::ConvertF32U32: case IrOpcode::ConvertF32F16: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: return true; case IrOpcode::BitCastU32F32: case IrOpcode::BitCastF32U32: @@ -67,6 +69,11 @@ private: case IrOpcode::SelectU32: case IrOpcode::SelectF32: return Contains(value.Argument(1)) && Contains(value.Argument(2)); + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPMinTri32: + case IrOpcode::FPMaxTri32: + case IrOpcode::FPMedTri32: case IrOpcode::Phi: for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { if (!Contains(value.Argument(index))) return false; From abe8fb81938394c3fe1d7e30f7549a96f8ce3473 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:45:30 -0700 Subject: [PATCH 005/353] fix(nid): validate ELF table layouts before patching --- core/libs/CMakeLists.txt | 2 ++ core/libs/nid/src/ElfNidPatcher.cpp | 13 +++++++ core/libs/nid/tests/test_elf_layout.py | 48 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 core/libs/nid/tests/test_elf_layout.py diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cfc..06b1f3c308 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -163,6 +163,8 @@ if(BUILD_TESTING) endif() find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $) endif() diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index 4f341bbd26..602037b847 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -15,6 +15,11 @@ namespace Nid { namespace { +void RequireElfRange(const std::vector& elf, std::uint64_t offset, std::uint64_t size, const char* table) { + if (offset > elf.size() || size > elf.size() - offset) + throw std::runtime_error(std::string("invalid ELF ") + table + " range"); +} + std::uint32_t GnuHash(const std::string& name) { std::uint32_t h = 5381u; for (const unsigned char c : name) @@ -316,6 +321,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& if (elf[4] != 2) throw std::runtime_error("not ELF64"); const auto ehdr = Read(elf, 0u); + if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr)) + throw std::runtime_error("unsupported ELF byte order or section header size"); + RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table"); std::size_t dynSymOffset = 0u, dynSymSize = 0u; std::size_t gnuHashOffset = 0u, gnuHashSize = 0u; @@ -326,6 +334,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); if (shdr.sh_type == kShtDynsym) { + if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum) + throw std::runtime_error("invalid ELF dynsym layout"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym"); dynSymOffset = static_cast(shdr.sh_offset); dynSymSize = static_cast(shdr.sh_size); dynSymSectionIndex = i; @@ -359,6 +370,8 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& { const std::size_t shOffset = static_cast(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); + if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr"); dynStrOffset = static_cast(shdr.sh_offset); dynStrSize = static_cast(shdr.sh_size); } diff --git a/core/libs/nid/tests/test_elf_layout.py b/core/libs/nid/tests/test_elf_layout.py new file mode 100644 index 0000000000..703ac46db3 --- /dev/null +++ b/core/libs/nid/tests/test_elf_layout.py @@ -0,0 +1,48 @@ +import struct +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, symbols, patch, SHOFF + + +class ElfLayoutTests(unittest.TestCase): + def test_declared_layout_must_match_supported_tables(self): + changes = ((5, "B", 2), (0x3a, "H", 32), + (SHOFF + 64 + 56, "Q", 8), + (SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1), + (SHOFF + 128 + 4, "I", 1)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def test_table_ranges_and_links_are_checked(self): + changes = ((0x28, "Q", 0x10000), + (SHOFF + 64 + 24, "Q", 0xff0), + (SHOFF + 64 + 40, "I", 100), + (SHOFF + 128 + 24, "Q", 0xff0), + (SHOFF + 128 + 32, "Q", 0xffffffffffffffff)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def reject(self, image): + result, after = patch(PATCHER, bytes(image), "layout.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr") + self.assertEqual(after, image) + + def test_valid_supported_layout_still_patches(self): + image = build_image(symbols(False), False) + result, after = patch(PATCHER, bytes(image), "valid-layout.so") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotEqual(after, image) + + +if __name__ == "__main__": + PATCHER = Path(sys.argv.pop(1)).resolve() + unittest.main() From 617e7acfa895b1d575dfddadfebfe36c8e175e5e Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:46:59 -0700 Subject: [PATCH 006/353] test(nid): avoid competing hash test registration --- core/libs/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 06b1f3c308..29c712034d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -163,8 +163,6 @@ if(BUILD_TESTING) endif() find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) - add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $) endif() @@ -175,6 +173,10 @@ if(BUILD_TESTING) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) endif() add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) + endif() add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp) target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) From 8ca4c10096c381b6ee54701e2ac15eb0a621c5a8 Mon Sep 17 00:00:00 2001 From: mugurc Date: Mon, 12 Oct 2026 15:11:01 +0300 Subject: [PATCH 007/353] build(macos): configure the full build for x86-64 under Rosetta The libraries are x86-64 code that runs under Rosetta 2 on Apple silicon, so the full build selects x86-64 itself: CMAKE_OSX_ARCHITECTURES defaults to x86_64, CMake is told the host processor is x86_64 instead of arm64, and libjpeg-turbo, which is configured by its own CMake run, gets the same values. Configuring the full build for another architecture fails with an error. Apple clang's libc++ needs -fexperimental-library for the C++20 library features the sources use. The prx libraries find each other through their run path. dyld does not know $ORIGIN, so on macOS it is @loader_path. --- CMakeLists.txt | 13 +++++++++++++ core/Decoder/Jpeg/CMakeLists.txt | 7 +++++++ core/libs/CMakeLists.txt | 10 ++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa3c450ddb..682b556a87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,8 @@ cmake_minimum_required(VERSION 3.20) +if(CMAKE_HOST_APPLE AND NOT ANYPS5_RELINKER_ONLY) + set(CMAKE_APPLE_SILICON_PROCESSOR x86_64 CACHE STRING "Host processor CMake reports on Apple silicon") + set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "Architectures built on macOS") +endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") @@ -47,6 +51,11 @@ elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() +if(APPLE AND NOT ANYPS5_RELINKER_ONLY) + add_compile_options($<$:-fexperimental-library>) + add_link_options(-fexperimental-library) +endif() + if(BUILD_TESTING) add_test_executable(shader_cache_file_read_tests ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp) @@ -100,6 +109,10 @@ if(ANYPS5_RELINKER_ONLY) return() endif() +if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + message(FATAL_ERROR "The full build on macOS is x86-64 only; configure without CMAKE_OSX_ARCHITECTURES or set it to x86_64") +endif() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index fcb338b8c0..f16c5df8a6 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -16,6 +16,12 @@ function(add_jpeg_decoder target runtimeTarget) set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") endif() file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include") + set(jpeg_turbo_platform_args) + if(APPLE) + list(APPEND jpeg_turbo_platform_args + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -DCMAKE_APPLE_SILICON_PROCESSOR=${CMAKE_APPLE_SILICON_PROCESSOR}) + endif() ExternalProject_Add(jpeg_turbo_external SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo" BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build" @@ -38,6 +44,7 @@ function(add_jpeg_decoder target runtimeTarget) -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + ${jpeg_turbo_platform_args} BUILD_BYPRODUCTS "${jpeg_turbo_library}" ) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cfc..aec21d2c53 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -9,6 +9,12 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() +if(APPLE) + set(ANYPS5_PRX_RPATH "@loader_path") +else() + set(ANYPS5_PRX_RPATH "$ORIGIN") +endif() + add_executable(nid_patcher ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp @@ -76,7 +82,7 @@ set_target_properties(libc PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) @@ -131,7 +137,7 @@ foreach(LIB_DIR ${LIBS_DIRS}) LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx) From f818d5e2250ad4f962197c67c81d6bc0aa91643a Mon Sep 17 00:00:00 2001 From: mugurc Date: Mon, 12 Oct 2026 15:16:14 +0300 Subject: [PATCH 008/353] fix(libs): spell inline assembly symbols for Mach-O hosts C symbols in Mach-O carry a leading underscore, and its assembler has no .type or .size directives. APS5_ASM_SYMBOL names a symbol the way the assembler of the host expects it, APS5_ASM_FUNCTION_END closes a function (.size on ELF, nothing elsewhere), and APS5_ASM_FUNCTION defines a global function on macOS. The fiber stack switch, the signal mask helpers, getcontext and their tests use them instead of writing the ELF spelling. The assembly they produce on Linux and Windows is the same as before. --- core/libs/prx/libSceFiber/Export.cpp | 20 ++--- .../prx/libc/include/general/AsmMacros.hpp | 10 ++- .../prx/libkernel/System/src/SignalSets.cpp | 8 +- .../prx/libkernel/System/src/UserContext.cpp | 2 +- core/libs/tests/GuestSignalSets.cpp | 8 +- core/libs/tests/GuestUserContext.cpp | 82 +++++++++---------- 6 files changed, 66 insertions(+), 64 deletions(-) diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 5935f1bad8..fb758ae871 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include #include #include @@ -200,11 +201,8 @@ static void UnpinStack(const void* context, std::uint64_t bytes) { extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); extern "C" void Aps5FiberTrampoline_nid_no_patch(); -asm(R"( - .text - .globl Aps5FiberSwitchStack_nid_no_patch - .type Aps5FiberSwitchStack_nid_no_patch, @function -Aps5FiberSwitchStack_nid_no_patch: +asm(".text\n" + APS5_ASM_FUNCTION("Aps5FiberSwitchStack_nid_no_patch") R"( push %rbp push %rbx push %r12 @@ -226,17 +224,13 @@ Aps5FiberSwitchStack_nid_no_patch: pop %rbx pop %rbp ret - .size Aps5FiberSwitchStack_nid_no_patch, .-Aps5FiberSwitchStack_nid_no_patch - - .globl Aps5FiberTrampoline_nid_no_patch - .type Aps5FiberTrampoline_nid_no_patch, @function -Aps5FiberTrampoline_nid_no_patch: +)" APS5_ASM_FUNCTION_END("Aps5FiberSwitchStack_nid_no_patch") + APS5_ASM_FUNCTION("Aps5FiberTrampoline_nid_no_patch") R"( mov %r12, %rdi and $-16, %rsp - call Aps5FiberMain_nid_no_patch + call )" APS5_ASM_SYMBOL("Aps5FiberMain_nid_no_patch") R"( ud2 - .size Aps5FiberTrampoline_nid_no_patch, .-Aps5FiberTrampoline_nid_no_patch -)"); +)" APS5_ASM_FUNCTION_END("Aps5FiberTrampoline_nid_no_patch")); struct InitialFrame { std::uint32_t mxcsr; diff --git a/core/libs/prx/libc/include/general/AsmMacros.hpp b/core/libs/prx/libc/include/general/AsmMacros.hpp index 0962f0c917..78de31dbd6 100644 --- a/core/libs/prx/libc/include/general/AsmMacros.hpp +++ b/core/libs/prx/libc/include/general/AsmMacros.hpp @@ -1,10 +1,18 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP -#ifdef _WIN32 +#if defined(_WIN32) +#define APS5_ASM_SYMBOL(name) name #define APS5_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" +#elif defined(__APPLE__) +#define APS5_ASM_SYMBOL(name) "_" name +#define APS5_ASM_FUNCTION(name) ".globl _" name "\n_" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" #else +#define APS5_ASM_SYMBOL(name) name #define APS5_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) ".size " name ", .-" name "\n" #endif #endif diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 871866cb31..f6e9eaa926 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -60,11 +61,10 @@ int APS5_VABI sigismember_nid_postfix(const GuestSignalSet* set, int signal) { return (set->bits[WordIndex(signal)] & Bit(signal)) != 0 ? 1 : 0; } } -#ifdef _WIN32 -#define APS5_SIGNAL_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" -#define APS5_SIGNAL_ASM_CALL(name) " call " name "\n" +#define APS5_SIGNAL_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define APS5_SIGNAL_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" #else -#define APS5_SIGNAL_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" #define APS5_SIGNAL_ASM_CALL(name) " call " name "@PLT\n" #endif diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 30593de7c3..92eaa7a389 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -107,7 +107,7 @@ asm(".text\n" " mov %rax, 0x98(%rsp)\n" " fxsave64 0xa0(%rsp)\n" " mov %rsp, %rsi\n" - " call Aps5GetcontextFinish_nid_no_patch\n" + " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" " mov %rbp, %rsp\n" " pop %rbp\n" " lea 8(%rsp), %rsp\n" diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index 798c736518..ddbea3ad02 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -23,11 +24,10 @@ int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSe int APS5_VABI JumpKeepingCalleeSaved(std::uint64_t* buffer); } -#ifdef _WIN32 -#define TEST_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" -#define TEST_ASM_CALL(name) " call " name "\n" +#define TEST_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define TEST_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" #else -#define TEST_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" #define TEST_ASM_CALL(name) " call " name "@PLT\n" #endif diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 41a4b5e0b9..74263f370a 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -61,9 +61,9 @@ asm(".text\n" " movabsq $0xcccccccccccccccc, %r11\n" " movabsq $0xdddddddddddddddd, %rdx\n" " movabsq $0xeeeeeeeeeeeeeeee, %rax\n" - " mov %rsp, CapturedStackPointer(%rip)\n" + " mov %rsp, " APS5_ASM_SYMBOL("CapturedStackPointer") "(%rip)\n" " stc\n" - " call getcontext_nid_postfix\n" + " call " APS5_ASM_SYMBOL("getcontext_nid_postfix") "\n" APS5_ASM_FUNCTION("CaptureReturnAddress") " add $8, %rsp\n" " pop %r15\n" @@ -75,50 +75,50 @@ asm(".text\n" " ret\n" APS5_ASM_FUNCTION("RecordRegisters") " pushfq\n" - " mov %rax, RecordedRegisters+0x00(%rip)\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x00(%rip)\n" " pop %rax\n" - " mov %rax, RecordedRegisters+0x88(%rip)\n" - " mov %rbx, RecordedRegisters+0x08(%rip)\n" - " mov %rcx, RecordedRegisters+0x10(%rip)\n" - " mov %rdx, RecordedRegisters+0x18(%rip)\n" - " mov %rsi, RecordedRegisters+0x20(%rip)\n" - " mov %rdi, RecordedRegisters+0x28(%rip)\n" - " mov %rbp, RecordedRegisters+0x30(%rip)\n" - " mov %rsp, RecordedRegisters+0x38(%rip)\n" - " mov %r8, RecordedRegisters+0x40(%rip)\n" - " mov %r9, RecordedRegisters+0x48(%rip)\n" - " mov %r10, RecordedRegisters+0x50(%rip)\n" - " mov %r11, RecordedRegisters+0x58(%rip)\n" - " mov %r12, RecordedRegisters+0x60(%rip)\n" - " mov %r13, RecordedRegisters+0x68(%rip)\n" - " mov %r14, RecordedRegisters+0x70(%rip)\n" - " mov %r15, RecordedRegisters+0x78(%rip)\n" - " stmxcsr RecordedMxcsr(%rip)\n" - " fnstcw RecordedControlWord(%rip)\n" - " movdqu %xmm0, RecordedXmm+0x00(%rip)\n" - " movdqu %xmm1, RecordedXmm+0x10(%rip)\n" - " movdqu %xmm2, RecordedXmm+0x20(%rip)\n" - " movdqu %xmm3, RecordedXmm+0x30(%rip)\n" - " movdqu %xmm4, RecordedXmm+0x40(%rip)\n" - " movdqu %xmm5, RecordedXmm+0x50(%rip)\n" - " movdqu %xmm6, RecordedXmm+0x60(%rip)\n" - " movdqu %xmm7, RecordedXmm+0x70(%rip)\n" - " movdqu %xmm8, RecordedXmm+0x80(%rip)\n" - " movdqu %xmm9, RecordedXmm+0x90(%rip)\n" - " movdqu %xmm10, RecordedXmm+0xa0(%rip)\n" - " movdqu %xmm11, RecordedXmm+0xb0(%rip)\n" - " movdqu %xmm12, RecordedXmm+0xc0(%rip)\n" - " movdqu %xmm13, RecordedXmm+0xd0(%rip)\n" - " movdqu %xmm14, RecordedXmm+0xe0(%rip)\n" - " movdqu %xmm15, RecordedXmm+0xf0(%rip)\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x88(%rip)\n" + " mov %rbx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x08(%rip)\n" + " mov %rcx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x10(%rip)\n" + " mov %rdx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x18(%rip)\n" + " mov %rsi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x20(%rip)\n" + " mov %rdi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x28(%rip)\n" + " mov %rbp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x30(%rip)\n" + " mov %rsp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x38(%rip)\n" + " mov %r8, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x40(%rip)\n" + " mov %r9, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x48(%rip)\n" + " mov %r10, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x50(%rip)\n" + " mov %r11, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x58(%rip)\n" + " mov %r12, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x60(%rip)\n" + " mov %r13, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x68(%rip)\n" + " mov %r14, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x70(%rip)\n" + " mov %r15, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x78(%rip)\n" + " stmxcsr " APS5_ASM_SYMBOL("RecordedMxcsr") "(%rip)\n" + " fnstcw " APS5_ASM_SYMBOL("RecordedControlWord") "(%rip)\n" + " movdqu %xmm0, " APS5_ASM_SYMBOL("RecordedXmm") "+0x00(%rip)\n" + " movdqu %xmm1, " APS5_ASM_SYMBOL("RecordedXmm") "+0x10(%rip)\n" + " movdqu %xmm2, " APS5_ASM_SYMBOL("RecordedXmm") "+0x20(%rip)\n" + " movdqu %xmm3, " APS5_ASM_SYMBOL("RecordedXmm") "+0x30(%rip)\n" + " movdqu %xmm4, " APS5_ASM_SYMBOL("RecordedXmm") "+0x40(%rip)\n" + " movdqu %xmm5, " APS5_ASM_SYMBOL("RecordedXmm") "+0x50(%rip)\n" + " movdqu %xmm6, " APS5_ASM_SYMBOL("RecordedXmm") "+0x60(%rip)\n" + " movdqu %xmm7, " APS5_ASM_SYMBOL("RecordedXmm") "+0x70(%rip)\n" + " movdqu %xmm8, " APS5_ASM_SYMBOL("RecordedXmm") "+0x80(%rip)\n" + " movdqu %xmm9, " APS5_ASM_SYMBOL("RecordedXmm") "+0x90(%rip)\n" + " movdqu %xmm10, " APS5_ASM_SYMBOL("RecordedXmm") "+0xa0(%rip)\n" + " movdqu %xmm11, " APS5_ASM_SYMBOL("RecordedXmm") "+0xb0(%rip)\n" + " movdqu %xmm12, " APS5_ASM_SYMBOL("RecordedXmm") "+0xc0(%rip)\n" + " movdqu %xmm13, " APS5_ASM_SYMBOL("RecordedXmm") "+0xd0(%rip)\n" + " movdqu %xmm14, " APS5_ASM_SYMBOL("RecordedXmm") "+0xe0(%rip)\n" + " movdqu %xmm15, " APS5_ASM_SYMBOL("RecordedXmm") "+0xf0(%rip)\n" " cld\n" - " lea ReturnContext(%rip), %rdi\n" + " lea " APS5_ASM_SYMBOL("ReturnContext") "(%rip), %rdi\n" " and $-16, %rsp\n" - " call setcontext_nid_postfix\n" + " call " APS5_ASM_SYMBOL("setcontext_nid_postfix") "\n" " ud2\n" APS5_ASM_FUNCTION("EntryThunk") - " mov %rsp, EntryStackPointer(%rip)\n" - " jmp ContextBody\n"); + " mov %rsp, " APS5_ASM_SYMBOL("EntryStackPointer") "(%rip)\n" + " jmp " APS5_ASM_SYMBOL("ContextBody") "\n"); namespace { From a671dc4edf816a5db3b1b5ace9bbe4bda20191f5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:36:06 +0200 Subject: [PATCH 009/353] fix(avplayer): resume the presentation clock when the source restarts after a pause Clock::Reset left the paused flag set by Clock::Pause, so a Start issued from the paused state (stop and restart of the source) kept the clock frozen at the start position. Only the frame at that position was delivered; later frames were held back by the default A/V sync. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAvPlayer/src/Source.cpp | 1 + core/libs/tests/GuestAvPlayer.cpp | 24 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 768358e690..7778568ddc 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -151,6 +151,7 @@ public: void Reset(double milliseconds) { base = milliseconds; running = false; + paused = false; } void Pause() { diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index d6ca109955..4ae36f4396 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1070,6 +1070,29 @@ void TestAutoStartKeepsStartedPlayback() { Check(startedByTitle, "auto-start won every race against the title's own start"); } +void TestRestartAfterPausedJumpKeepsClockRunning() { + AvPlayerInitData init = InitData(nullptr); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no video after start"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "looping rejected"); + Check(sceAvPlayerJumpToTime(player, 0) == 0, "jump while paused failed"); + Check(sceAvPlayerStart(player) == 0, "restart after the paused jump failed"); + std::uint64_t last = 0; + Check(WaitFor([&] { + if (sceAvPlayerGetVideoDataEx(player, &frame)) last = frame.timestamp; + return last >= 200; + }), "playback froze after a restart that followed a paused jump"); + Check(sceAvPlayerCurrentTime(player) > 0, "clock stayed frozen after the restart"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { Events events; AvPlayerInitData init = InitData(&events); @@ -1168,6 +1191,7 @@ int main() { TestHandedOutFramesStayIntact(); TestPs5ExtendedInitLayout(); TestAutoStartKeepsStartedPlayback(); + TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); TestEofStopLatency(); std::puts("AvPlayer tests passed"); From da6948ea182ecbfa8b05c56f88b39f1b3f44773b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:41:31 -0700 Subject: [PATCH 010/353] fix(libc): report host C++ exceptions that Windows cannot deliver libstdc++ and libgcc raise C++ exceptions as SEH exceptions (STATUS_GCC_THROW, 0x20474343), which the system dispatcher delivers by unwinding frame by frame with each function's unwind data. The libraries are built with -fno-asynchronous-unwind-tables and unwind with DWARF, so their functions have no SEH unwind data. When a host runtime call such as operator new throws straight into one of them, the dispatcher cannot unwind it: no handler, the unhandled exception filter or std::terminate runs, and the process ends without output and with exit code 0x20474343. The vectored crash handler now follows a GCC exception through the runtime frames that raise it (KERNELBASE, libgcc_s_seh-1, libstdc++-6). When the first frame outside them has no unwind data, it reports the exception's type, the thread and that frame, and lists the stack's return address candidates, before the dispatcher continues. The test loads libc.prx, then lets operator new throw std::bad_alloc into a function built with the libraries' flags. Without the handler change it exits silently with 0x20474343. --- core/libs/CMakeLists.txt | 8 ++++ .../src/specifics/windows/CrashReport.cpp | 47 +++++++++++++++++++ .../libs/tests/UndeliverableHostException.cpp | 29 ++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 core/libs/tests/UndeliverableHostException.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cfc..d6568cebd1 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -186,6 +186,14 @@ if(BUILD_TESTING) add_subdirectory(tests/write_watch) endif() + if(WIN32) + add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + add_dependencies(undeliverable_host_exception_tests libc) + add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $) + set_tests_properties(undeliverable_host_exception PROPERTIES + PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 53d651430f..104e208d2c 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -2,6 +2,7 @@ #include #include #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -223,6 +225,47 @@ void TraceSse4a(std::uint64_t rip, const sse4a::Instruction& instruction, const Report("[sse4a] #%llu %s %s (length %u, index %u, %u bytes) at %s on thread %lu\n", count, mnemonic, operands, field.length, field.index, static_cast(instruction.size), line, GetCurrentThreadId()); } +constexpr DWORD GccExceptionMask = 0x20ffffffu; +constexpr DWORD GccException = 0x20474343u; +constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; +constexpr std::uint64_t CxaExceptionTypeOffset = 80; + +std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { + const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; + CONTEXT context = start; + for (int depth = 0; depth < 64 && context.Rip != 0; ++depth) { + HMODULE module = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(context.Rip), &module); + DWORD64 imageBase = 0; + const auto entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr); + if (module == nullptr || std::find(std::begin(runtime), std::end(runtime), module) == std::end(runtime)) return entry == nullptr ? context.Rip : 0; + if (entry == nullptr) return 0; + PVOID handlerData = nullptr; + DWORD64 establisher = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handlerData, &establisher, nullptr); + } + return 0; +} + +void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { + const auto* record = info->ExceptionRecord; + char type[256] = "of unknown type"; + if (record->NumberParameters >= 1) { + const auto header = static_cast(record->ExceptionInformation[0]); + if (IsReadable(header) && IsReadable(header - CxaExceptionTypeOffset) && *reinterpret_cast(header) == GccExceptionClass) { + const auto typeInfo = *reinterpret_cast(header - CxaExceptionTypeOffset); + if (IsReadable(typeInfo) && IsReadable(typeInfo + 8)) { + const auto name = *reinterpret_cast(typeInfo + 8); + if (IsReadable(name)) std::snprintf(type, sizeof(type), "%s", reinterpret_cast(name)); + } + } + } + char line[MAX_PATH + 64]; + DescribeAddress(frame, line, sizeof(line)); + Report("\nFATAL: C++ exception %s raised on thread %lu cannot be delivered: %s has no unwind data\n", type, GetCurrentThreadId(), line); + ReportStackCandidates(info->ContextRecord->Rsp, 32, 0x4000); +} + bool HandleSse4a(EXCEPTION_POINTERS* info) { if (!g_sse4aEmulation) return false; const auto* record = info->ExceptionRecord; @@ -246,6 +289,10 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; + if ((record->ExceptionCode & GccExceptionMask) == GccException) { + if (const auto frame = FrameWithoutUnwindData(*info->ContextRecord)) ReportUndeliverableException(info, frame); + return EXCEPTION_CONTINUE_SEARCH; + } if (!IsFatal(record->ExceptionCode)) return EXCEPTION_CONTINUE_SEARCH; if (reported.exchange(true)) { // Another thread is already reporting; let it finish before this fault ends the process. diff --git a/core/libs/tests/UndeliverableHostException.cpp b/core/libs/tests/UndeliverableHostException.cpp new file mode 100644 index 0000000000..6356af4e7b --- /dev/null +++ b/core/libs/tests/UndeliverableHostException.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +namespace { + +[[gnu::noinline]] void* AllocateWithoutUnwindData(std::size_t size) { + void* const block = ::operator new(size); + std::printf("allocated %p\n", block); + return block; +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + AllocateWithoutUnwindData(std::size_t{1} << 46); + std::fputs("operator new returned instead of throwing\n", stderr); + return 1; +} From 0a1dff2fcc833cc2a9e0a6ea2d3c20cee9084ad3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:26:10 -0700 Subject: [PATCH 011/353] perf(agc): read the draw snapshot cap when resources are built ShaderResources::PrepareDrawBindings called MaxDrawSnapshotBytes() on every draw, and it read APS5_DRAW_SNAPSHOT_MAX_MIB with getenv each time. The UCRT's getenv takes the environment lock and scans the whole block. In Planet of Lana (about 90,000 draws per 10 s) that call was 2.6% of the queue-0 worker's samples. The cap is now read once when a ShaderResources object is built and kept in it, so a cached resource template uses it for every draw it serves. In Planet of Lana the record phase drops from 21.2 to 18.7 us per draw. drawSnapshotCapTests also raises the variable after building the resources and checks that they keep the cap they were built with. With the previous ShaderResources.cpp it fails there. --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b29f6df0c..1d56dc3a45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -434,6 +434,7 @@ private: std::unique_ptr bda; bool usesBda = false; bool usesFaultBuffer = false; + std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); VkDescriptorSetLayout _layout = VK_NULL_HANDLE; // Whether the layout is this object's own (no cache) and destroyed with it. bool ownsLayout = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 63bd7b18ec..0fffafe641 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3515,7 +3515,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); - const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); + const auto maxSnapshotBytes = maxDrawSnapshotBytes; std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 938b001028..21f64005a9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1871,6 +1871,19 @@ void drawSnapshotCapTests(const Device& device, Recorder& recorder) { Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write"); std::memset(block, 0x11, bytes); snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap"); +#endif + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with"); + snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap"); +#endif } const auto firstAddress = address + 4096; constexpr std::size_t firstBytes = 600 * 1024; From ed56cff7904bea615b174ffbf659d10de98846ed Mon Sep 17 00:00:00 2001 From: Zayd Zaari <110066668+zaydzaari@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:49:09 +0000 Subject: [PATCH 012/353] fix(libkernel): accept measured system version record values Preserve the leading field and bytes after the 40-byte legacy version record for the values measured in four PS5 firmware 13.42 hardware runs. Keep the emulator version policy unchanged and leave the separately proposed Prospero correction out of scope. AI-assisted; regression tests are based on guarded hardware measurements. --- .../System/src/SystemConfiguration.cpp | 3 -- core/libs/tests/GuestSystemConfiguration.cpp | 37 +++++++++---------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c5c0f22207..9ce9eb63f1 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -199,9 +199,6 @@ int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* if (version == nullptr) { return SCE_KERNEL_ERROR_EFAULT; } - if (version->size != sizeof(SceKernelSystemSwVersion)) { - return SCE_KERNEL_ERROR_EINVAL; - } std::memset(version->versionString, 0, sizeof(version->versionString)); std::memcpy(version->versionString, "01.000.000", sizeof("01.000.000")); version->version = 0x01000000; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 069ef27671..42fb187b65 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -139,26 +139,25 @@ static void CheckPathnameSysctl() { static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); - SceKernelSystemSwVersion version{}; - version.size = 0; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - version.size = sizeof(SceKernelSystemSwVersion) - 1; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - version.size = sizeof(SceKernelSystemSwVersion) + 1; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - std::memset(&version, 0xaa, sizeof(version)); - version.size = sizeof(SceKernelSystemSwVersion); - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == 0 && *__error_nid_postfix() == 13); - Require(version.size == sizeof(SceKernelSystemSwVersion)); - Require(std::strcmp(version.versionString, "01.000.000") == 0); - for (std::size_t i = sizeof("01.000.000"); i < sizeof(version.versionString); ++i) { - Require(version.versionString[i] == 0); + const std::uint64_t sizes[] = {0, 24, 39, 40, 41, 48, 64}; + for (const std::uint64_t size : sizes) { + struct { + std::uint64_t prefix; + SceKernelSystemSwVersion version; + unsigned char suffix[88]; + } storage; + std::memset(&storage, 0xaa, sizeof(storage)); + storage.version.size = size; + *__error_nid_postfix() = 13; + Require(sceKernelGetSystemSwVersion_nid_postfix(&storage.version) == 0 && *__error_nid_postfix() == 13); + Require(storage.prefix == 0xaaaaaaaaaaaaaaaaull && storage.version.size == size); + Require(std::strcmp(storage.version.versionString, "01.000.000") == 0); + for (std::size_t i = sizeof("01.000.000"); i < sizeof(storage.version.versionString); ++i) { + Require(storage.version.versionString[i] == 0); + } + Require(storage.version.version == 0x01000000); + for (unsigned char byte : storage.suffix) Require(byte == 0xaa); } - Require(version.version == 0x01000000); } static void CheckPseudoRandomSysctl() { *__error_nid_postfix() = 13; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 132b6b9b42..8e0a4b9c98 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -15,7 +15,7 @@ - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it -- [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. +- [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it From 6a9781cdc6c41731c0db36478d5f0cebab80f748 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:29:46 -0700 Subject: [PATCH 013/353] fix(libc): read the C++ exception metadata with bounded reads The undeliverable-exception report checked only that the first byte of each value it read was in readable memory: the exception class, the type_info pointer, the name pointer and the RTTI name, which it then printed with %s, scanning to the terminator without a bound. A name that runs into an unreadable page, or damaged exception metadata, could fault inside the crash handler. Every read now goes through ReadCode, which stops at the first unreadable page and at the requested size. The name is copied into a 256-byte buffer and used only if the copy holds its terminator; otherwise the report keeps "of unknown type". --- .../src/specifics/windows/CrashReport.cpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 104e208d2c..80c742647c 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -247,18 +247,21 @@ std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { return 0; } +bool ReadWord(std::uint64_t address, std::uint64_t* value) { + return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); +} + void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { const auto* record = info->ExceptionRecord; char type[256] = "of unknown type"; - if (record->NumberParameters >= 1) { - const auto header = static_cast(record->ExceptionInformation[0]); - if (IsReadable(header) && IsReadable(header - CxaExceptionTypeOffset) && *reinterpret_cast(header) == GccExceptionClass) { - const auto typeInfo = *reinterpret_cast(header - CxaExceptionTypeOffset); - if (IsReadable(typeInfo) && IsReadable(typeInfo + 8)) { - const auto name = *reinterpret_cast(typeInfo + 8); - if (IsReadable(name)) std::snprintf(type, sizeof(type), "%s", reinterpret_cast(name)); - } - } + const auto header = record->NumberParameters >= 1 ? static_cast(record->ExceptionInformation[0]) : 0; + std::uint64_t exceptionClass = 0; + std::uint64_t typeInfo = 0; + std::uint64_t namePointer = 0; + if (ReadWord(header, &exceptionClass) && exceptionClass == GccExceptionClass && ReadWord(header - CxaExceptionTypeOffset, &typeInfo) && ReadWord(typeInfo + 8, &namePointer)) { + std::uint8_t name[sizeof(type)]; + const auto copied = ReadCode(namePointer, name, sizeof(name)); + if (const auto* end = static_cast(std::memchr(name, 0, copied))) std::memcpy(type, name, static_cast(end - name) + 1); } char line[MAX_PATH + 64]; DescribeAddress(frame, line, sizeof(line)); From 85cf9c3ac7b6ed641c7ac6b789cd3ae1bf06aecf Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:37:58 +0200 Subject: [PATCH 014/353] fix(tools): preserve technical debt section moves --- tools/debt_split.py | 2 +- tools/tests/test_debt_split.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index bfb7f9d522..c344192ece 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -170,7 +170,7 @@ def apply_change(root, base, new, index_text): position = entries.index(anchored) + 1 entries.insert(position, moved) - matcher = difflib.SequenceMatcher(None, [entry[2] for entry in before], [entry[2] for entry in after], autojunk=False) + matcher = difflib.SequenceMatcher(None, [entry[1:3] for entry in before], [entry[1:3] for entry in after], autojunk=False) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "equal": continue diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 1b5c9acc3e..a9bb72e276 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -110,6 +110,27 @@ class DebtSplitTests(unittest.TestCase): self.assertFalse((self.root / "docs/dev/debt/libSceC.md").exists()) self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_moves_unchanged_entry_to_functional(self): + result = self.rebase([A_STUB, B_STUB], [C_STUB, A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceC.md"), + "# libSceC technical debt\n\n### Functional\n\n" + C_STUB.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceC.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + def test_resolve_moves_unchanged_entry_to_silent_stubs(self): + result = self.rebase([A_STUB, B_STUB, C_STUB, A_FUNCTIONAL], [SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + + "\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceA.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_refuses_changes_to_lines_that_stay_in_the_index(self): result = self.rebase([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED + " too"]) self.assertNotEqual(result.returncode, 0) From 44d46ee97c98f3d9f4ec31c69a9634363243e20a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 18:30:30 +0000 Subject: [PATCH 015/353] fix(libSceNet): accept MSG_DONTWAIT in the send and receive calls sceNetSend, sceNetSendto, sceNetRecv, sceNetRecvfrom, sceNetRecvmsg and sceNetSendmsg failed with EOPNOTSUPP for any flag other than MSG_PEEK on a receive, so a game polling a socket with MSG_DONTWAIT got an error on every call instead of its data or EWOULDBLOCK. FreeBSD 11.4 defines the flag in sys/sys/socket.h: #define MSG_DONTWAIT 0x80 /* this message should be nonblocking */ and soreceive_generic() (sys/kern/uipc_socket.c) treats it like a non-blocking socket for that one call: if ((so->so_state & SS_NBIO) || (flags & (MSG_DONTWAIT|MSG_NBIO))) { SOCKBUF_UNLOCK(&so->so_rcv); error = EWOULDBLOCK; goto release; shadPS4's sceNet uses the same value (ORBIS_NET_MSG_DONTWAIT 0x80). On Linux and macOS the flag now maps to the host's MSG_DONTWAIT, alone or with MSG_PEEK on a receive. Winsock has no per-call non-blocking flag, so Windows keeps failing with EOPNOTSUPP for it; TechnicalDebt records that. Other flags still fail with EOPNOTSUPP. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 38 +++++++++++++++++-------- core/libs/tests/GuestSceNet.cpp | 45 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 1ea3b505ab..30a7dd7685 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -89,6 +89,7 @@ constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; +constexpr int NET_MSG_DONTWAIT = 0x80; constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 @@ -460,6 +461,21 @@ int receive_error(const Sock& socket) { return native_error(); } +bool message_flags_supported(int flags, int accepted) { +#ifdef _WIN32 + accepted &= ~NET_MSG_DONTWAIT; +#endif + return (flags & ~accepted) == 0; +} + +int native_message_flags(int flags) { + int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; +#ifndef _WIN32 + if (flags & NET_MSG_DONTWAIT) native |= MSG_DONTWAIT; +#endif + return native; +} + } // namespace extern "C" { @@ -768,7 +784,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { } int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -780,7 +796,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); - const int native_flags = flags == 2 ? MSG_PEEK : 0; + const int native_flags = native_message_flags(flags); const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); @@ -789,7 +805,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); if (from && !fromlen) return fail(NET_EINVAL); @@ -804,7 +820,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const int native_flags = flags == 2 ? MSG_PEEK : 0; + const int native_flags = native_message_flags(flags); int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); #ifdef _WIN32 @@ -816,7 +832,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* } int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -826,12 +842,12 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); + const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -844,13 +860,13 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); + result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS, + result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), reinterpret_cast(&destination), destination_length); } return result >= 0 ? result : fail(native_error()); @@ -877,7 +893,7 @@ int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { const auto total = message_length(msg); if (total < 0) return total; - if (flags != 0 && flags != NET_MSG_PEEK) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); Sock socket; { std::lock_guard lk(g_mutex); @@ -895,7 +911,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), - flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); if (received < 0) { #ifdef _WIN32 if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 2ad9a8a605..0329656637 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -210,6 +210,50 @@ static void CheckEmptyEpoll() { } } +static void CheckDontWait() { + constexpr int DontWait = 0x80; + constexpr int Peek = 0x2; + const int receiver = sceNetSocket("dontwait-receiver", 2, 2, 17); + const int sender = sceNetSocket("dontwait-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0); + const int timeout = 2000000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[16]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; +#ifdef _WIN32 + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 45)); + Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 45)); + Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 45)); + Require(Failed(sceNetSendto(sender, "x", 1, DontWait, address.data(), address.size()), 45)); +#else + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); + Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 35)); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek), 35)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), DontWait, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(receiver, buffer, sizeof(buffer), Peek, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(sceNetConnect(sender, address.data(), address.size()) == 0); + Require(sceNetSend(sender, datagram, sizeof(datagram), DontWait) == sizeof(datagram)); + Require(sceNetRecvmsg(receiver, &receive, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); +#endif + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0x1), 45)); + Require(Failed(sceNetSend(sender, "x", 1, Peek), 45)); + Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); +} + static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { const int timeout = 50000; Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); @@ -476,6 +520,7 @@ int main() { Require(sceNetInit_nid_postfix() == 0); CheckPoolStats(); CheckEmptyEpoll(); + CheckDontWait(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 346d3b3e7d..c74270f7cc 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -10,4 +10,5 @@ - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From c35572ec18dd068286a574fd17bf3ceee49e2e67 Mon Sep 17 00:00:00 2001 From: PrintOrb Date: Mon, 12 Oct 2026 00:17:17 -0500 Subject: [PATCH 016/353] fix(audioout): serialize shared SDL audio initialization --- core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp | 6 ++++++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 9 ++++++++- core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp | 5 +++-- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp diff --git a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp new file mode 100644 index 0000000000..f0e8c87cc4 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP + +int InitializeSdlAudio_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index c8adb77b2a..db1ecd88b5 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -16,6 +16,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; @@ -103,11 +104,17 @@ static std::mutex g_outputMutex; static Port g_ports[PORTS_MAX]; static bool g_sdlInitialized = false; +int InitializeSdlAudio_nid_no_patch() { + static std::mutex initializationMutex; + std::lock_guard lock(initializationMutex); + return SDL_InitSubSystem(SDL_INIT_AUDIO); +} + static bool ensureSdlAudio() { if (g_sdlInitialized) { return true; } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { return false; } g_sdlInitialized = true; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 383ad018f7..3f88277444 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -14,6 +14,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "AudioOut2Internal.hpp" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" // An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) // to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so @@ -106,7 +107,7 @@ static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now static void DeviceCallback(void* userdata, Uint8* stream, int length); static void OpenDevice(AudioOut2Context& context) { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); return; } @@ -134,7 +135,7 @@ static void CloseDevice(SDL_AudioDeviceID device) { } static void OpenPadDevice(AudioOut2Context& context) { - if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return; + if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && InitializeSdlAudio_nid_no_patch() < 0) return; const int count = SDL_GetNumAudioDevices(0); for (int index = 0; index < count; index++) { const char* listed = SDL_GetAudioDeviceName(index, 0); From 5031432db0d39c8c95504218673166cf6a69ef1a Mon Sep 17 00:00:00 2001 From: PrintOrb Date: Mon, 12 Oct 2026 16:04:06 -0500 Subject: [PATCH 017/353] test(audioout): detect overlapping backend initialization --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 12 ++++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 6 -- .../prx/libSceAudioOut/src/AudioOutSdl.cpp | 10 +++ .../tests/AudioOutInitialization.cpp | 65 +++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index bc11e8e5b1..3ef08cfa11 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp @@ -17,6 +18,17 @@ target_link_libraries(libSceAudioOut PRIVATE libc libkernel SDL2-static) configure_windows_unwind(libSceAudioOut) if(BUILD_TESTING) + add_test_executable(audio_out_initialization_tests tests/AudioOutInitialization.cpp src/AudioOutSdl.cpp) + target_include_directories(audio_out_initialization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_initialization_tests PRIVATE libc) + configure_windows_unwind(audio_out_initialization_tests) + add_test(NAME audio_out_initialization COMMAND audio_out_initialization_tests) + set_tests_properties(audio_out_initialization PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out_initialization PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_pad_mix_tests tests/AudioOut2PadMix.cpp src/AudioOut2PadMix.cpp) target_include_directories(audio_out2_pad_mix_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_pad_mix_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index db1ecd88b5..dee5a712e3 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -104,12 +104,6 @@ static std::mutex g_outputMutex; static Port g_ports[PORTS_MAX]; static bool g_sdlInitialized = false; -int InitializeSdlAudio_nid_no_patch() { - static std::mutex initializationMutex; - std::lock_guard lock(initializationMutex); - return SDL_InitSubSystem(SDL_INIT_AUDIO); -} - static bool ensureSdlAudio() { if (g_sdlInitialized) { return true; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp new file mode 100644 index 0000000000..45f41f5a43 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp @@ -0,0 +1,10 @@ +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" + +#include "SDL.h" +#include + +int InitializeSdlAudio_nid_no_patch() { + static std::mutex initializationMutex; + std::lock_guard lock(initializationMutex); + return SDL_InitSubSystem(SDL_INIT_AUDIO); +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp new file mode 100644 index 0000000000..8ae9cd327c --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp @@ -0,0 +1,65 @@ +#define SDL_MAIN_HANDLED +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "SDL.h" + +#include +#include +#include +#include +#include +#include + +namespace { +std::mutex stateMutex; +std::condition_variable stateChanged; +int calls = 0; +int active = 0; +int peak = 0; +bool secondEntered = false; +bool secondReady = false; +bool validFlags = true; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32 flags) { + std::unique_lock lock(stateMutex); + validFlags = validFlags && flags == SDL_INIT_AUDIO; + const int call = ++calls; + ++active; + if (active > peak) peak = active; + stateChanged.notify_all(); + if (call == 1) { + stateChanged.wait(lock, [] { return secondReady; }); + stateChanged.wait_for(lock, std::chrono::milliseconds(500), [] { return secondEntered; }); + } else { + secondEntered = true; + stateChanged.notify_all(); + } + --active; + return call == 1 ? -1 : 0; +} + +int main() { + int firstResult = 99; + int secondResult = 99; + std::thread first([&] { firstResult = InitializeSdlAudio_nid_no_patch(); }); + { + std::unique_lock lock(stateMutex); + stateChanged.wait(lock, [] { return calls == 1; }); + } + std::thread second([&] { + { + std::lock_guard lock(stateMutex); + secondReady = true; + stateChanged.notify_all(); + } + secondResult = InitializeSdlAudio_nid_no_patch(); + }); + first.join(); + second.join(); + if (peak != 1 || calls != 2 || active != 0 || !validFlags || firstResult != -1 || secondResult != 0) { + std::fprintf(stderr, "audio initialization overlap or return-value mismatch: peak=%d calls=%d results=%d,%d\n", peak, calls, firstResult, secondResult); + return 1; + } + std::puts("Serialized audio initialization: PASS"); + return 0; +} From 7f39ac991b60ec31fc17c0863166210f8c303be5 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 12 Oct 2026 23:02:16 +0200 Subject: [PATCH 018/353] fix(conventions): keep #### subsections inside the pull request template sections SECTION matched every heading that starts with ###, so a #### line opened a new section named "# ...". A Tested section written as #### Linux and #### Windows subsections was reported as missing or empty, and an unchecked item after a #### line in Checklist was not reported. A level-4 heading belongs to the level-3 section above it (CommonMark 4.2), and silent_stub_debt() already keeps deeper headings inside their section. SECTION now matches only headings with exactly three #. Co-Authored-By: Claude Opus 5.5 --- tools/check_conventions.py | 2 +- tools/tests/test_check_conventions_comments.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index de46dd4d96..3b31b916fd 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -45,7 +45,7 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\]\(([^)\s]+)") -SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) +SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 4d3e83c0bc..00601b4086 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -246,13 +246,15 @@ class SilentStubSectionTests(unittest.TestCase): class PullRequestTemplateTests(unittest.TestCase): - def findings(self, line): + def body_findings(self, body): check = Check.__new__(Check) check.findings = [] - body = f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n" check.pull_request("fix: x", body) return check.findings + def findings(self, line): + return self.body_findings(f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n") + def test_unchecked_items_with_any_bullet_are_reported(self): for bullet in ("-", "*", "+"): with self.subTest(bullet=bullet): @@ -261,6 +263,14 @@ class PullRequestTemplateTests(unittest.TestCase): def test_checked_item_is_accepted(self): self.assertEqual(self.findings("- [x] all"), []) + def test_subsections_stay_inside_their_section(self): + body = "### What\nx\n### Tested\n#### Linux\nctest\n#### Windows\nctest\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), []) + + def test_unchecked_item_after_a_checklist_subsection_is_reported(self): + body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n#### Later\n- [ ] all\n" + self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "unchecked: all")]) + class SpacedPathTests(unittest.TestCase): def test_comment_in_path_with_space_is_reported(self): From 565fafa85d21b9c32ee7a5cbd46130389da0249e Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Tue, 13 Oct 2026 03:19:27 +0530 Subject: [PATCH 019/353] feat(net): implement memory-pool netstat diagnostic --- core/libs/prx/libSceNet/Export.cpp | 19 ++++++++++ core/libs/tests/GuestSceNet.cpp | 58 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 1ea3b505ab..0b1536db67 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1047,6 +1047,25 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { return id; } +int APS5_VABI sceNetShowNetstatWithMemory(int memid) { + struct SocketStatus { int id; int family; int type; bool nonblock; bool bound; bool listening; }; + std::vector sockets; + std::size_t poolSize = 0; + { + std::lock_guard lk(g_mutex); + const auto pool = g_pools.find(memid); + if (pool == g_pools.end()) return fail(NET_EBADF); + poolSize = pool->second; + sockets.reserve(g_socks.size()); + for (const auto& [id, socket] : g_socks) + sockets.push_back({id, socket.family, socket.type, socket.nonblock, socket.bound, socket.listening}); + } + std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); + for (const auto& socket : sockets) + std::fprintf(stdout, "socket=%d family=%d type=%d nonblock=%d bound=%d listening=%d\n", socket.id, socket.family, socket.type, int(socket.nonblock), int(socket.bound), int(socket.listening)); + return 0; +} + int APS5_VABI sceNetGetMemoryPoolStats(int memid, NetMemoryPoolStats* stats) { std::lock_guard lk(g_mutex); const auto pool = g_pools.find(memid); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 2ad9a8a605..eb48a55281 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN +#include #include #include #else @@ -20,12 +21,14 @@ #include #include #include +#include #include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); +int APS5_VABI sceNetShowNetstatWithMemory(int); int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); @@ -161,6 +164,60 @@ static void CheckStreamSender(int family) { for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static std::string CaptureNetstat(int pool) { + auto* file = std::tmpfile(); + Require(file != nullptr); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + const int output = _fileno(stdout); + const int saved = _dup(output); + Require(saved >= 0 && _dup2(_fileno(file), output) == 0); +#else + const int output = fileno(stdout); + const int saved = dup(output); + Require(saved >= 0 && dup2(fileno(file), output) == output); +#endif + const int result = sceNetShowNetstatWithMemory(pool); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + Require(_dup2(saved, output) == 0 && _close(saved) == 0); +#else + Require(dup2(saved, output) == output && close(saved) == 0); +#endif + Require(result == 0 && std::fseek(file, 0, SEEK_SET) == 0); + std::string text; + std::array bytes{}; + for (std::size_t count; (count = std::fread(bytes.data(), 1, bytes.size(), file)) != 0;) text.append(bytes.data(), count); + Require(std::ferror(file) == 0 && std::fclose(file) == 0); + return text; +} + +static void CheckNetstat() { + Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); + const int pool = sceNetPoolCreate("netstat", 4096, 0); + Require(pool > 0); + const auto empty = CaptureNetstat(pool); + Require(empty.find("bytes=4096 sockets=0") != std::string::npos); + const int socket = sceNetSocket("netstat", 2, 1, 0); + Require(socket >= 0); + const int nonblocking = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(socket, address.data(), address.size()) == 0); + Require(sceNetListen(socket, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == address.size()); + const auto before = address; + const auto snapshot = CaptureNetstat(pool); + Require(snapshot.find("bytes=4096 sockets=1") != std::string::npos); + Require(snapshot.find("socket=" + std::to_string(socket) + " family=2 type=1 nonblock=1 bound=1 listening=1") != std::string::npos); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && address == before); + Require(sceNetSocketClose(socket) == 0); + Require(CaptureNetstat(pool).find("sockets=0") != std::string::npos); + Require(sceNetPoolDestroy(pool) == 0); + Require(Failed(sceNetShowNetstatWithMemory(pool), 9)); +} + static void CheckPoolStats() { const int pool = sceNetPoolCreate("stats", 0x4000, 0); Require(pool > 0); @@ -475,6 +532,7 @@ int main() { } Require(sceNetInit_nid_postfix() == 0); CheckPoolStats(); + CheckNetstat(); CheckEmptyEpoll(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); From 7ee6e4af2083cce3c0b8377c6fc2d5acb266375c Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Mon, 12 Oct 2026 18:05:53 -0400 Subject: [PATCH 020/353] fix(shader): load the PERSP_SAMPLE and LINEAR_SAMPLE I/J pairs The pixel entry prologue filled only the center and centroid pairs, so a shader interpolating through a sample pair read I = J = 0 and every attribute came out as its P0. Give the sample pairs their SPI_PS_INPUT_ADDR VGPRs, fill them with the barycentrics of their kind, and classify attributes read through LINEAR_SAMPLE as NoPerspective for host interpolation. --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 20 ++++++++++++++++--- .../Translation/src/AttributeInstructions.cpp | 4 ++-- .../Translation/src/InstructionTranslator.cpp | 3 ++- .../src/ShaderInputInfoBuilder.cpp | 2 ++ docs/dev/debt/shader.md | 1 + 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cc37014883..f2a151677d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -704,10 +704,10 @@ bool readsBuiltin(const std::vector& read, spv::BuiltIn builtin) { return std::find(read.begin(), read.end(), builtin) != read.end(); } -std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) { +std::vector pixelNoPerspectiveLocations(bool barycentricEnabled, std::uint32_t inputs = 0x22u) { auto queue = makeState(); - queue.context[0x1b3] = 0x22u; - queue.context[0x1b4] = 0x22u; + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; queue.context[0x1b6] = 2u; queue.context[0x191] = 0u; queue.context[0x192] = 1u; @@ -741,6 +741,7 @@ std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) } } if (barycentricEnabled) Require(perVertex == 2u && perspectiveBarycentrics && linearBarycentrics, "explicit interpolation must preserve both parameter vertices and both barycentric inputs"); + if (!barycentricEnabled) Require(!result.barycentricEmulation.active, "v_interp_p1/p2 of their own I/J pairs did not keep host interpolation"); std::vector result2; for (const auto id : noPerspective) result2.push_back(locations.count(id) != 0 ? locations.at(id) : 0xffffffffu); return result2; @@ -951,6 +952,19 @@ void PixelInputLayoutTests() { const auto noPerspective = pixelNoPerspectiveLocations(false); Require(noPerspective.size() == 1 && noPerspective[0] == 1u, "only the parameter interpolated through the linear pair must be NoPerspective"); Require(pixelNoPerspectiveLocations(true).empty(), "explicit interpolation must not interpolate parameter arrays a second time"); + for (const auto inputs : {0x11u, 0x12u, 0x21u}) { + const auto sampleNoPerspective = pixelNoPerspectiveLocations(false, inputs); + Require(sampleNoPerspective.size() == 1 && sampleNoPerspective[0] == 1u, "a parameter interpolated through a sample I/J pair lost its interpolation mode: inputs " + std::to_string(inputs)); + Require(pixelNoPerspectiveLocations(true, inputs).empty(), "explicit interpolation through a sample I/J pair interpolated parameter arrays a second time: inputs " + std::to_string(inputs)); + } + for (const auto source : {0u, 1u}) { + const auto read = pixelBuiltinsRead(0x3u, 0x3u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "PERSP_SAMPLE does not hold the perspective barycentrics: v" + std::to_string(source)); + } + for (const auto source : {2u, 3u}) { + const auto read = pixelBuiltinsRead(0x12u, 0x12u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "LINEAR_SAMPLE does not hold the linear barycentrics: v" + std::to_string(source)); + } auto read = pixelBuiltinsRead(0x106u, 0x106u, 4u); Require(readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "POS_X is not in v4 after the center and centroid pairs"); read = pixelBuiltinsRead(0x326u, 0x326u, 5u); diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index 514aa328d8..f2878ed016 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -78,9 +78,9 @@ void TranslationContext::vInterpP2F32(const RdnaInstruction& inst) { return base != ShaderPixelInputInfo::NoPixelInputVgpr && inst.source0.reg == base + 1u; }; const auto bit = 1u << inst.source1.value; - if (readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { + if (readsPair(PixelInput::LinearSample) || readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { program.Metadata().pixelLinearInputs |= bit; - } else if (readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { + } else if (readsPair(PixelInput::PerspectiveSample) || readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { program.Metadata().pixelPerspectiveInputs |= bit; } } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index e3455f44ea..7a306786aa 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -340,7 +340,8 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* ps = options.inputInfo.pixel; const auto vgpr = [&](PixelInput input) { return ps->psInputVgpr[static_cast(input)]; }; const auto loaded = [&](PixelInput input) { return vgpr(input) != ShaderPixelInputInfo::NoPixelInputVgpr; }; - for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, + for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveSample, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, + std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearSample, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { if (!loaded(input)) continue; setVector(static_cast(vgpr(input)), builtin(kind, 0u)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ce067c4913..eb76fbfc02 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -111,8 +111,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } pixelStorage.psInputVgpr[static_cast(input)] = PixelInputVgpr(pixel.inputAddr, input); }; + place(PixelInput::PerspectiveSample, (pixel.inputAddr & PixelInputBit(PixelInput::PerspectiveSample)) != 0u); place(PixelInput::PerspectiveCenter, pixel.hasPerspectiveCenterVgpr); place(PixelInput::PerspectiveCentroid, pixel.perspectiveCentroid); + place(PixelInput::LinearSample, (pixel.inputAddr & PixelInputBit(PixelInput::LinearSample)) != 0u); place(PixelInput::LinearCenter, pixel.noPerspective); place(PixelInput::LinearCentroid, pixel.linearCentroid); place(PixelInput::PositionX, pixel.posX); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d28e018661..6a10ee1006 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -96,6 +96,7 @@ - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. +- [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. Whole-word uses still throw. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. From 819c47526645de69eff1f8ed7b685dc95ec1dbcb Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 15:10:22 -0700 Subject: [PATCH 021/353] fix(tools): match explicit trace summaries to their capture --- tools/tests/test_trace_digest.py | 13 +++++++++++++ tools/trace_digest.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index fd02c62e37..7917bc864a 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -64,6 +64,19 @@ class TraceDigestTests(unittest.TestCase): def test_summary_is_the_line_of_this_trace(self): self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") + def test_explicit_summary_selects_the_matching_trace(self): + summary = Path(self.directory.name) / "relocated-summary.jsonl" + other = dict(SUMMARY, trace="other.json.gz", presents=99) + summary.write_text(json.dumps(SUMMARY) + "\n" + json.dumps(other) + "\n", encoding="utf-8") + selected = trace_digest.load_summary(summary, self.trace) + self.assertEqual(selected["trace"], self.trace.name) + self.assertEqual(selected["presents"], 3) + + def test_explicit_summary_without_matching_trace_is_not_used(self): + summary = Path(self.directory.name) / "unrelated-summary.jsonl" + summary.write_text(json.dumps(dict(SUMMARY, trace="other.json.gz")) + "\n", encoding="utf-8") + self.assertIsNone(trace_digest.load_summary(summary, self.trace)) + def test_frames_end_at_the_window(self): result = self.result() self.assertEqual(result["frames"], 3) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 99011db35e..a375a776b1 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -67,7 +67,7 @@ def load_summary(path, trace_path): return None lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] for line in reversed(lines): - if path or line.get("trace") == Path(trace_path).name: + if line.get("trace") == Path(trace_path).name: return line return None From 52c71b837c4db867a364ddc9f7ce9e22d884520e Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:40:52 +0200 Subject: [PATCH 022/353] feat(agc): add APS5_TRACE_SUMMARY_EVERY for summary lines of a long session APS5_TRACE_SUMMARY_EVERY= appends one line to aps5-summary.jsonl every seconds, at a present, from the per-scope totals the trace threads already keep. No event buffer is allocated unless a window switch is set as well; then the window records its full trace on top of the intervals and its own summary line holds only the time inside the window. Each line has the fields of the window summary line for its interval plus interval_start_s and interval_end_s. tools/trace_digest.py prints a summary file with interval lines as one row per interval with a text sparkline of the frame p99 and the GPU busy time. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/PerformanceTrace.hpp | 71 ++++++++- .../Execution/src/PerformanceTrace.cpp | 135 +++++++++++++++--- .../tests/PerformanceTrace.cpp | 76 ++++++++++ docs/dev/BUILD.md | 12 +- tools/tests/test_trace_digest.py | 58 ++++++++ tools/trace_digest.py | 66 ++++++++- 6 files changed, 394 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index b859482795..0cefddf624 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -84,6 +84,44 @@ private: Clock::duration openedAt{}; }; +class TraceInterval { +public: + using Clock = std::chrono::steady_clock; + + static std::optional Parse(const char* seconds) { + if (seconds == nullptr) return std::nullopt; + const std::string_view text = seconds; + std::uint64_t length = 0; + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, length); + if (text.empty() || error != std::errc{} || last != end || length == 0 || length > 31536000) + throw std::invalid_argument("APS5_TRACE_SUMMARY_EVERY=" + std::string(text) + " is not a whole number of seconds above 0"); + return TraceInterval(length); + } + + bool Due(Clock::duration elapsed) const { + return elapsed - begun >= std::chrono::seconds(length); + } + + void Restart(Clock::duration elapsed) { + begun = elapsed; + } + + Clock::duration Begun() const { + return begun; + } + + std::string Describe() const { + return "summary_every " + std::to_string(length); + } + +private: + explicit TraceInterval(std::uint64_t length) : length(length) {} + + std::uint64_t length; + Clock::duration begun{}; +}; + class TraceThread { public: using Clock = std::chrono::steady_clock; @@ -111,8 +149,10 @@ public: bool dropped = false; }; - TraceThread(std::string name, const std::atomic& presents, std::size_t capacity = DefaultCapacity) - : name(std::move(name)), presents(presents), slices(std::make_unique(capacity)), capacity(capacity) {} + inline static const std::atomic AlwaysBuffering{true}; + + TraceThread(std::string name, const std::atomic& presents, std::size_t capacity = DefaultCapacity, const std::atomic& buffering = AlwaysBuffering) + : name(std::move(name)), presents(presents), buffering(buffering), slices(std::make_unique(capacity)), capacity(capacity) {} TraceThread(const TraceThread&) = delete; TraceThread& operator=(const TraceThread&) = delete; @@ -120,6 +160,7 @@ public: void Add(const char* scope, const char* stage, Clock::time_point begin, Clock::time_point end) { const auto length = end - begin; account(scope, stage, length); + if (!buffering.load(std::memory_order_relaxed)) return; const auto present = presents.load(std::memory_order_relaxed); if (!sampled || present != sampledPresent) { if (sampled) append({nullptr, nullptr, sampleBegin, folded}); @@ -182,6 +223,7 @@ private: const std::string name; const std::atomic& presents; + const std::atomic& buffering; const std::unique_ptr slices; const std::size_t capacity; std::atomic used{0}; @@ -206,6 +248,7 @@ struct TraceSummary { std::uint64_t firstPresent = 0; double gpuBusyMs = 0; std::string trace; + std::optional> interval; }; struct TraceGpuRange { @@ -307,6 +350,21 @@ public: return slices; } + static std::vector Since(std::span now, std::span before) { + std::vector result; + for (const auto& total : now) { + auto delta = total; + for (const auto& old : before) { + if (old.scope != total.scope || old.stage != total.stage) continue; + delta.length -= old.length; + delta.count -= old.count; + break; + } + if (delta.count != 0) result.push_back(delta); + } + return result; + } + static std::string Summary(const TraceSummary& summary, std::span threads, std::span presents) { const auto count = presents.empty() ? std::size_t{0} : presents.size() - 1; const auto span = presents.empty() ? Clock::duration{} : presents.back() - presents.front(); @@ -339,6 +397,7 @@ public: AppendString(output, summary.window); output += ",\"first_present\":" + std::to_string(summary.firstPresent); output += ",\"ms\":" + number(milliseconds(span)) + "}"; + if (summary.interval) output += ",\"interval_start_s\":" + number(summary.interval->first) + ",\"interval_end_s\":" + number(summary.interval->second); output += ",\"presents\":" + std::to_string(count); output += ",\"frame_ms\":{\"p50\":" + number(percentile(0.5)) + ",\"p90\":" + number(percentile(0.9)) + ",\"p99\":" + number(percentile(0.99)) + ",\"max\":" + number(frames.empty() ? 0.0 : frames.back()) + "}"; output += ",\"gpu_busy_ms_per_present\":" + number(summary.gpuBusyMs * perPresent); @@ -396,7 +455,7 @@ private: class PerformanceTrace { public: static bool Configured() { - static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr; return configured; } @@ -404,7 +463,7 @@ public: static void Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device); static void GpuRange(const char* name, double beginNs, double endNs) { - if (!Recording()) return; + if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { gpuDropped = true; @@ -424,7 +483,11 @@ public: static void StopOutput(); private: + static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present); + static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device); + inline static std::atomic recording{false}; + inline static std::atomic buffering{false}; inline static std::atomic presentCount{0}; inline static thread_local TraceThread* local = nullptr; inline static std::mutex gpuMutex; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index d2320c3f5c..6d1b13254a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -30,7 +30,16 @@ constexpr std::size_t GpuRangeCapacity = TraceThread::DefaultCapacity; struct Session { std::mutex mutex; std::vector> threads; + bool started = false; std::optional window; + bool windowOpen = false; + std::vector> windowBase; + std::optional interval; + bool intervalOpen = false; + std::uint64_t intervalFirstPresent = 0; + double intervalGpuBusy = 0; + std::vector> intervalBase; + std::vector intervalPresents; Clock::time_point first{}; std::uint64_t present = 0; std::uint64_t firstPresent = 0; @@ -49,6 +58,16 @@ std::unique_ptr& writer() { return output; } +std::unique_ptr& summaryWriter() { + static std::unique_ptr output; + return output; +} + +std::filesystem::path traceDirectory() { + const char* directory = std::getenv("APS5_TRACE_DIR"); + return std::filesystem::path(directory != nullptr ? directory : "."); +} + std::uint64_t processId() { #ifdef _WIN32 return GetCurrentProcessId(); @@ -91,29 +110,109 @@ std::vector> environment() { return result; } +TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + TraceSummary summary; + summary.commit = APS5_TRACE_COMMIT; + if (APS5_TRACE_DIRTY >= 0) summary.dirty = APS5_TRACE_DIRTY != 0; + summary.environment = environment(); + summary.gpu = device != nullptr ? device->DeviceName() : std::string(); + summary.driver = device != nullptr ? device->DriverInfo() : std::string(); + summary.os = operatingSystem(); + summary.width = width; + summary.height = height; + return summary; +} + +std::vector> totalsOf(const std::vector>& threads) { + std::vector> result; + for (const auto& thread : threads) result.push_back(thread->Collect().totals); + return result; +} + +double seconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); +} + } void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { if (!Configured() || local != nullptr) return; - auto bound = std::make_unique(name, presentCount, capacity); + const bool windowed = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + auto bound = std::make_unique(name, presentCount, windowed ? capacity : 0, buffering); auto& state = session(); std::lock_guard lock(state.mutex); local = bound.get(); state.threads.push_back(std::move(bound)); } +void PerformanceTrace::OpenInterval(Clock::time_point now, std::uint64_t present) { + auto& state = session(); + state.interval->Restart(now - state.first); + state.intervalOpen = true; + state.intervalFirstPresent = present; + state.intervalGpuBusy = VulkanDevice::PresentCounts().gpuBusyMs; + state.intervalPresents.assign(1, now); + if (summaryWriter()) return; + summaryWriter() = std::make_unique([directory = traceDirectory()](std::string_view text) { + try { + std::filesystem::create_directories(directory); + TraceFile::AppendLine(directory / "aps5-summary.jsonl", text); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] appending to aps5-summary.jsonl failed: %s\n", error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] summarizing %s from present %llu\n", state.interval->Describe().c_str(), static_cast(present)); +} + +void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + auto& state = session(); + state.intervalBase.resize(state.threads.size()); + std::vector snapshots; + for (std::size_t index = 0; index < state.threads.size(); ++index) { + auto snapshot = state.threads[index]->Collect(); + auto totals = std::move(snapshot.totals); + snapshot.slices = {}; + snapshot.totals = TraceFormat::Since(totals, state.intervalBase[index]); + state.intervalBase[index] = std::move(totals); + snapshots.push_back(std::move(snapshot)); + } + auto summary = identity(width, height, device); + summary.window = state.interval->Describe(); + summary.firstPresent = state.intervalFirstPresent; + summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy; + summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); + summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); +} + void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { const auto now = Clock::now(); BindThread("presenter"); auto& state = session(); std::lock_guard lock(state.mutex); - if (!state.window) { - state.window.emplace(TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")).value()); + if (!state.started) { + state.window = TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")); + state.interval = TraceInterval::Parse(std::getenv("APS5_TRACE_SUMMARY_EVERY")); state.first = now; + state.started = true; } const auto present = state.present++; - const auto edge = state.window->Present(present, now - state.first); + const auto edge = state.window ? state.window->Present(present, now - state.first) : TraceWindow::Edge::None; + if (state.interval) { + if (!state.intervalOpen) { + OpenInterval(now, present); + } else { + state.intervalPresents.push_back(now); + if (state.interval->Due(now - state.first)) { + FlushInterval(now, width, height, device); + OpenInterval(now, present); + } + } + } if (edge == TraceWindow::Edge::Open) { + state.windowOpen = true; + state.windowBase = totalsOf(state.threads); state.firstPresent = present; state.gpuBusyAtOpen = VulkanDevice::PresentCounts().gpuBusyMs; TraceClockSample clock; @@ -125,21 +224,31 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const } state.presents.push_back(now); presentCount.fetch_add(1, std::memory_order_relaxed); + buffering.store(true, std::memory_order_relaxed); recording.store(true, std::memory_order_relaxed); ProfilePrint_nid_no_patch("[trace] recording %s from present %llu%s\n", state.window->Describe().c_str(), static_cast(present), state.gpuClock ? "" : "; no GPU track: the device has no calibrated timestamps"); return; } - if (!recording.load(std::memory_order_relaxed)) return; + if (!state.windowOpen) { + if (!state.interval) return; + presentCount.fetch_add(1, std::memory_order_relaxed); + recording.store(true, std::memory_order_relaxed); + return; + } state.presents.push_back(now); presentCount.fetch_add(1, std::memory_order_relaxed); if (edge != TraceWindow::Edge::Close) return; - recording.store(false, std::memory_order_relaxed); + state.windowOpen = false; + buffering.store(false, std::memory_order_relaxed); + recording.store(state.interval.has_value(), std::memory_order_relaxed); std::vector snapshots; std::size_t presenter = 0; + state.windowBase.resize(state.threads.size()); for (std::size_t index = 0; index < state.threads.size(); ++index) { if (state.threads[index].get() == local) presenter = index; snapshots.push_back(state.threads[index]->Collect()); + snapshots.back().totals = TraceFormat::Since(snapshots.back().totals, state.windowBase[index]); } std::vector gpuSlices; TraceClockSample clock; @@ -149,23 +258,14 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); } const auto process = processId(); - TraceSummary summary; - summary.commit = APS5_TRACE_COMMIT; - if (APS5_TRACE_DIRTY >= 0) summary.dirty = APS5_TRACE_DIRTY != 0; - summary.environment = environment(); - summary.gpu = device != nullptr ? device->DeviceName() : std::string(); - summary.driver = device != nullptr ? device->DriverInfo() : std::string(); - summary.os = operatingSystem(); - summary.width = width; - summary.height = height; + auto summary = identity(width, height, device); summary.window = state.window->Describe(); summary.firstPresent = state.firstPresent; summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.gpuBusyAtOpen; summary.trace = "aps5-trace-" + summary.commit + "-" + std::to_string(process) + ".json.gz"; auto events = TraceFormat::Events(process, state.presents.front(), snapshots, presenter, state.presents, state.firstPresent); - const char* traceDirectory = std::getenv("APS5_TRACE_DIR"); auto& output = writer(); - output = std::make_unique([directory = std::filesystem::path(traceDirectory != nullptr ? traceDirectory : "."), name = summary.trace, line = TraceFormat::Summary(summary, snapshots, state.presents)](std::string_view text) { + output = std::make_unique([directory = traceDirectory(), name = summary.trace, line = TraceFormat::Summary(summary, snapshots, state.presents)](std::string_view text) { try { std::filesystem::create_directories(directory); TraceFile::WriteGzip(directory / name, text); @@ -184,6 +284,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const void PerformanceTrace::StopOutput() { if (writer()) writer()->Stop(); + if (summaryWriter()) summaryWriter()->Stop(); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 25c9a62bed..28c5136504 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -153,6 +153,78 @@ void GpuClock() { Check(TraceFormat::GpuSlices(ranges, same, same, origin)[0].begin == origin + microseconds(14), "one calibration does not map at the CPU rate"); } +void IntervalDue() { + Check(!AgcDriver::TraceInterval::Parse(nullptr).has_value(), "an interval was made without a switch"); + for (const auto* text : {"", "0", "-5", "1.5", "a", "5s", "1:2", "99999999999"}) { + bool rejected = false; + try { AgcDriver::TraceInterval::Parse(text); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed interval was accepted"); + } + auto interval = AgcDriver::TraceInterval::Parse("30").value(); + Check(interval.Describe() == "summary_every 30", "interval description changed"); + Check(!interval.Due(milliseconds(29999)) && interval.Due(milliseconds(30000)), "the interval was due at the wrong time"); + interval.Restart(milliseconds(30500)); + Check(interval.Begun() == milliseconds(30500) && !interval.Due(milliseconds(60499)) && interval.Due(milliseconds(60500)), "a restarted interval did not count from its restart"); +} + +void SummaryOnlyThread() { + std::atomic presents{1}; + std::atomic buffering{false}; + TraceThread thread("queue worker 0x0", presents, 0, buffering); + const auto origin = Clock::now(); + thread.Add("Driver.Packet", nullptr, origin, origin + milliseconds(2)); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(3), origin + milliseconds(4)); + presents.store(2); + thread.Add("Driver.Completion", nullptr, origin + milliseconds(5), origin + microseconds(5005)); + auto snapshot = thread.Collect(); + Check(snapshot.slices.empty() && !snapshot.dropped, "a thread that is not buffering kept a slice"); + Check(snapshot.totals.size() == 2 && snapshot.totals[0].count + snapshot.totals[1].count == 3, "a thread that is not buffering missed a total"); + buffering.store(true); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(6), origin + milliseconds(7)); + Check(thread.Collect().slices.empty(), "a zero capacity buffer held a slice"); +} + +void TotalsSince() { + const std::vector before{{"Driver.Packet", nullptr, milliseconds(4), 2}, {"Driver.Present", nullptr, milliseconds(1), 1}}; + const std::vector now{{"Driver.Packet", nullptr, milliseconds(10), 5}, {"Driver.Present", nullptr, milliseconds(1), 1}, {"Driver.Completion", nullptr, milliseconds(3), 2}}; + const auto delta = TraceFormat::Since(now, before); + Check(delta.size() == 2, "a total without a new slice was kept or a new one lost"); + Check(delta[0].length == milliseconds(6) && delta[0].count == 3, "a total was not reduced by the previous interval"); + Check(std::string_view(delta[1].scope) == "Driver.Completion" && delta[1].length == milliseconds(3) && delta[1].count == 2, "a total without a previous value changed"); + Check(TraceFormat::Since(now, {}).size() == 3, "the first interval dropped a total"); +} + +void IntervalSummaryFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = true; + summary.environment = {{"APS5_TRACE_SUMMARY_EVERY", "30"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.firstPresent = 120; + summary.gpuBusyMs = 20; + summary.interval = std::make_pair(30.5, 60.25); + const std::vector threads{ + {"queue worker 0x0", {}, {{"Driver.Packet", nullptr, milliseconds(12), 4}}, false}, + }; + const std::vector presents{origin, origin + milliseconds(16), origin + milliseconds(40)}; + const auto line = TraceFormat::Summary(summary, threads, presents); + const std::string expected = + "{\"commit\":\"abc\",\"dirty\":true,\"env\":{\"APS5_TRACE_SUMMARY_EVERY\":\"30\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"resolution\":\"1920x1080\",\"window\":{\"spec\":\"summary_every 30\",\"first_present\":120,\"ms\":40.000},\"interval_start_s\":30.500,\"interval_end_s\":60.250,\"presents\":2," + "\"frame_ms\":{\"p50\":24.000,\"p90\":24.000,\"p99\":24.000,\"max\":24.000},\"gpu_busy_ms_per_present\":10.000," + "\"worker_busy_pct\":{\"queue worker 0x0\":30.000}," + "\"top_phases_ms_per_frame\":{\"Driver.Packet\":6.000}," + "\"trace_dropped\":false,\"trace\":\"\"}\n"; + Check(line == expected, "interval summary line changed"); +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -210,6 +282,10 @@ int main() { EventsShape(); SummaryFields(); GpuClock(); + IntervalDue(); + SummaryOnlyThread(); + TotalsSince(); + IntervalSummaryFields(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 82b5576701..2ced340bc7 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -140,7 +140,17 @@ The shader preparation threads are traced as well, `shader prepare`, with a buff The `gpu` track holds one `batch` slice per recorder batch, from the batch's timestamp pair. With `APS5_PROFILE_GPU=1` it also holds a slice per command class range, under the class names of the `[gputime]` line (`draw`, `copy`, `fill`, `present-blit`, ...); program ranges are left out. The track keeps up to 262,144 ranges; more set `trace_dropped`. The GPU clock is mapped onto the CPU clock with calibrated timestamps (`VK_KHR_calibrated_timestamps` or the EXT version, enabled only when a trace switch is set) taken when the window opens and when it closes, so the two clocks are matched at both ends of the window. A batch is added when the driver reads its timestamps, after its fence signals, so the batches of the last frames of the window, which complete after it closes, are missing. Without calibrated timestamps the trace has no `gpu` track, and the `[trace]` line says so. -With either switch set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. +With any of the three switches set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. + +### Summaries for a long session + +Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed value fails the first present) to profile a long session in the background without a trace. The first present opens the first interval. Every `` seconds, at the first present at least that long after the interval opened, the presenter appends one line to `aps5-summary.jsonl` in `APS5_TRACE_DIR` or the working directory and opens the next interval at that same present, so the intervals follow each other without a gap and their `frame_ms` cover every interval between presents. The line has the fields of the window summary line for that interval only, plus `interval_start_s` and `interval_end_s` (seconds since the first present). Its `window` is `{"spec":"summary_every ","first_present":,"ms":}` and its `trace` is the empty string. No event buffer is allocated: each thread keeps only the per-scope totals it already folds (a few kilobytes), and the presenter subtracts the totals of the previous interval. An interval that is still open when the title exits writes nothing. + +The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The intervals run for the whole session and the window records its full trace on top: the event buffers are allocated and filled only while the window is open, the window's own summary line (with `trace` set and no `interval_*` fields) holds only the time inside the window, and the interval lines continue unchanged around it. Without a window switch no buffer is allocated at all. The two switches do not share a clock origin: `interval_*` is relative to the first present, as the window's `APS5_TRACE_SECONDS` start is. + +`python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. + +With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. ## Driver thread priority diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index fd02c62e37..d64da9fa8b 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -125,5 +125,63 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(json.loads(output.getvalue())["frames"], 3) +def interval_line(start, end, p99, gpu_ms, **changes): + line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, + presents=1800, frame_ms={"p50": 16.6, "p90": 18.0, "p99": p99, "max": p99 + 5.0}, gpu_busy_ms_per_present=gpu_ms, trace="") + line.update(changes) + return line + + +class IntervalDigestTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.path = Path(self.directory.name) / "aps5-summary.jsonl" + lines = [SUMMARY, interval_line(0.0, 30.0, 20.0, 5.0), interval_line(30.0, 60.5, 40.0, 10.0, trace_dropped=True), + interval_line(60.5, 90.5, 0.0, 0.0, worker_busy_pct={}, top_phases_ms_per_frame={}), interval_line(90.5, 120.5, 20.0, 5.0, gpu="Other GPU")] + self.path.write_text("\n".join(json.dumps(line) for line in lines) + "\n", encoding="utf-8") + + def tearDown(self): + self.directory.cleanup() + + def test_window_lines_are_skipped(self): + self.assertEqual(len(trace_digest.load_intervals(self.path)), 4) + + def test_rows(self): + result = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) + first, second, third, _ = result["intervals"] + self.assertEqual((first["start_s"], first["end_s"], first["presents"]), (0.0, 30.0, 1800)) + self.assertEqual(first["busiest_worker"], {"thread": "queue worker 0x0", "pct": 50.0}) + self.assertEqual(first["top_phase"], {"name": "Driver.Packet", "ms_per_frame": 7.0}) + self.assertTrue(second["trace_dropped"]) + self.assertIsNone(third["busiest_worker"]) + self.assertIsNone(third["top_phase"]) + self.assertEqual(result["identity_changed_at"], [3]) + + def test_sparkline_scales_to_the_largest_value(self): + self.assertEqual(trace_digest.sparkline([0.0, 5.0, 10.0]), ".+@") + self.assertEqual(trace_digest.sparkline([0.0, 0.0]), "..") + self.assertEqual(trace_digest.sparkline([]), "") + + def test_command_line_table_and_json(self): + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path)]), 0) + text = output.getvalue() + self.assertIn("**4 intervals**, 0.0 to 120.5 s, 7200 presents", text) + self.assertIn("| 2 | 30-60 (dropped) | 1800 | 16.6 | 18.0 | 40.0 | 45.0 | 10.0 | 50 % | Driver.Packet 7.0 |", text) + self.assertIn("p99 ms +@.+ 0-40.0", text) + self.assertIn("changed at interval 4", text) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.path), "--json"]) + self.assertEqual(len(json.loads(output.getvalue())["intervals"]), 4) + + def test_file_without_intervals(self): + self.path.write_text(json.dumps(SUMMARY) + "\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.path)]) + self.assertIn("no interval lines", output.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 99011db35e..e68c5d00d8 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -10,7 +10,13 @@ DESCRIPTION = """Digest of an APS5_TRACE_FRAMES / APS5_TRACE_SECONDS capture (ap pull request: the summary line, the slowest frames with their largest slices, the busy and wait time of each thread, the largest slices per frame, the longest waits and, with --stacks, the hottest functions of an APS5_SAMPLE_WORKER collapsed-stack file. The summary line is read from aps5-summary.jsonl next to the trace -unless --summary names another file.""" +unless --summary names another file. + +A summary file written by APS5_TRACE_SUMMARY_EVERY (aps5-summary.jsonl, or any .jsonl) is digested as a table with +one row per interval and a text sparkline of the frame p99 and of the GPU busy time per present; the lines of +window captures in the same file are skipped.""" + +RAMP = "._:-=+*#%@" def is_wait(name): @@ -203,15 +209,71 @@ def markdown(result): return "\n".join(lines) + "\n" +def load_intervals(path): + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + return [line for line in lines if "interval_start_s" in line] + + +def sparkline(values): + high = max(values, default=0.0) + if high <= 0: + return RAMP[0] * len(values) + return "".join(RAMP[min(len(RAMP) - 1, int(value / high * (len(RAMP) - 1) + 0.5))] for value in values) + + +def digest_intervals(lines): + rows = [] + for line in lines: + workers = line.get("worker_busy_pct", {}) + busiest = max(workers.items(), key=lambda entry: entry[1]) if workers else None + phases = list(line.get("top_phases_ms_per_frame", {}).items()) + rows.append({"start_s": line["interval_start_s"], "end_s": line["interval_end_s"], "presents": line["presents"], + "frame_ms": line["frame_ms"], "gpu_busy_ms_per_present": line["gpu_busy_ms_per_present"], + "busiest_worker": {"thread": busiest[0], "pct": busiest[1]} if busiest else None, + "top_phase": {"name": phases[0][0], "ms_per_frame": phases[0][1]} if phases else None, + "trace_dropped": line.get("trace_dropped", False)}) + first = lines[0] if lines else {} + keys = ("commit", "dirty", "env", "gpu", "driver", "os", "resolution") + changed = [index for index, line in enumerate(lines) if any(line.get(key) != first.get(key) for key in keys)] + return {"intervals": rows, "identity": {key: first.get(key) for key in keys}, "identity_changed_at": changed} + + +def markdown_intervals(result): + rows = result["intervals"] + identity = result["identity"] + if not rows: + return "no interval lines (interval_start_s) found\n" + lines = [f"**{len(rows)} intervals**, {rows[0]['start_s']:.1f} to {rows[-1]['end_s']:.1f} s, {sum(row['presents'] for row in rows)} presents", + f"{identity['gpu']}, {identity['driver']}, {identity['os']}, {identity['resolution']}, commit {identity['commit']}" + f"{' (dirty)' if identity.get('dirty') else ''}"] + if result["identity_changed_at"]: + lines.append("commit, environment, GPU, driver or OS changed at interval " + ", ".join(str(index + 1) for index in result["identity_changed_at"])) + lines += ["", "| # | seconds | presents | p50 | p90 | p99 | max | GPU busy | busiest worker | largest phase |", "|---|---|---|---|---|---|---|---|---|---|"] + for index, row in enumerate(rows, 1): + frame = row["frame_ms"] + worker = f"{row['busiest_worker']['pct']:.0f} %" if row["busiest_worker"] else "-" + phase = f"{row['top_phase']['name']} {row['top_phase']['ms_per_frame']:.1f}" if row["top_phase"] else "-" + lines.append(f"| {index} | {row['start_s']:.0f}-{row['end_s']:.0f}{' (dropped)' if row['trace_dropped'] else ''} | {row['presents']} | {frame['p50']:.1f} | {frame['p90']:.1f} " + f"| {frame['p99']:.1f} | {frame['max']:.1f} | {row['gpu_busy_ms_per_present']:.1f} | {worker} | {phase} |") + p99 = [row["frame_ms"]["p99"] for row in rows] + gpu = [row["gpu_busy_ms_per_present"] for row in rows] + lines += ["", "```", f"p99 ms {sparkline(p99)} 0-{max(p99):.1f}", f"gpu ms {sparkline(gpu)} 0-{max(gpu):.1f}", "```"] + return "\n".join(lines) + "\n" + + def main(arguments=None): parser = argparse.ArgumentParser(description=DESCRIPTION) - parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json)") + parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt)") parser.add_argument("--frames", type=int, default=5, help="slowest frames to list (default 5)") parser.add_argument("--top", type=int, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) + if options.trace.endswith(".jsonl"): + result = digest_intervals(load_intervals(options.trace)) + sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) + return 0 result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), load_stacks(options.stacks) if options.stacks else None, options.frames, options.top) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) From 0900586191187cb24e07a0f8fe27287ad5b38e4f Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Tue, 13 Oct 2026 00:34:59 +0100 Subject: [PATCH 023/353] feat(agc): skip the draws and dispatches of one shader with APS5_SKIP_SHADER APS5_SKIP_SHADER= drops every draw that runs a program at that address and every dispatch of a compute program at that address, as if the packet had run. Skipping one pass at a time shows which pass first writes a wrong value into an image: Epic Mickey's (PPSA15657) green sky light was bisected this way to the copy into its sky cube (#4551). Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Driver/Diagnostics.hpp | 2 + .../Execution/src/Driver/Diagnostics.cpp | 6 ++ .../src/Driver/Dispatch/Dispatch.cpp | 1 + .../Execution/src/Driver/Draw/Draw.cpp | 2 + .../tests/execution/SkipShader.cpp | 71 +++++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 3c0093fbe8..3667d7bfd1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -8,6 +8,8 @@ namespace AgcDriver::DriverDetail { double TraceMs(); +std::uint64_t SkippedShader(); + void require(bool condition, const char* reason); std::uint32_t readRegister(const Registers& registers, std::uint32_t offset); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 59d038112b..3e80f40e9b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -9,6 +10,11 @@ double TraceMs() { return std::chrono::duration(std::chrono::steady_clock::now() - origin).count(); } +std::uint64_t SkippedShader() { + static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + return skipped; +} + void require(bool condition, const char* reason) { if (!condition) { throw std::runtime_error(std::string("AGC driver: ") + reason); 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 cc300de66e..88938ad682 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -16,6 +16,7 @@ namespace AgcDriver::DriverDetail { void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); + if (address == SkippedShader() && address != 0) return; auto registeredShader = RegisteredProgram(*submission.shaders, address, {0}); if (!registeredShader) registeredShader = ReadRawShader(address); const auto& snapshot = *registeredShader; 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 e8ecb282d6..9a5ffe639f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -94,6 +95,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& graphics = decode->state; auto pixel = decode->pixel; std::vector programs = decode->programs; + if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; auto& words = programs.front().userData; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp new file mode 100644 index 0000000000..377e819b46 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Skipped{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array Kept{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Stored = 0x12345678u; +alignas(256) std::array SkippedOutput{}; +alignas(256) std::array KeptOutput{}; + +void Dispatch(const std::array& code, std::array& output) { + const auto address = reinterpret_cast(output.data()); + const std::array data{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(output.size()), 0x00014204u}; + const auto program = reinterpret_cast(code.data()); + std::vector words{ + 0xc0037600u, 0x207, 64, 1, 1, + 0xc0027600u, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), + 0xc0017600u, 0x213, (4u << 1u) | (1u << 7u), + 0xc0047600u, 0x240, data[0], data[1], data[2], data[3], + 0xc0031500u, 1, 1, 1, 0x41u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(address, sizeof(output)); +} + +} + +int main() { + try { + char value[32]; + std::snprintf(value, sizeof(value), "%llx", static_cast(reinterpret_cast(Skipped.data()))); +#ifdef _WIN32 + Require(_putenv_s("APS5_SKIP_SHADER", value) == 0, "cannot set APS5_SKIP_SHADER"); +#else + Require(setenv("APS5_SKIP_SHADER", value, 1) == 0, "cannot set APS5_SKIP_SHADER"); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + SkippedOutput.fill(Untouched); + KeptOutput.fill(Untouched); + Dispatch(Skipped, SkippedOutput); + Dispatch(Kept, KeptOutput); + for (std::size_t i = 0; i < SkippedOutput.size(); ++i) { + Require(SkippedOutput[i] == Untouched, "the dispatch of the skipped shader stored dword " + std::to_string(i)); + Require(KeptOutput[i] == Stored, "the dispatch of another shader did not store dword " + std::to_string(i)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("skip shader tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 584c59e5ace5a2989f5e758daac4a5ac60417bd5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:18:28 -0700 Subject: [PATCH 024/353] test(libkernel): compare the unterminated readv buffer with memcmp --- core/libs/tests/GuestUdp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index fb9acba813..2f11de31af 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -118,7 +118,7 @@ int main() { char vectorSecond[sizeof(vectorTail)]{}; Iovec vectorInput[]{{vectorFirst, sizeof(vectorFirst)}, {vectorSecond, sizeof(vectorSecond)}}; Require(sceKernelReadv(receiver, vectorInput, 2) == sizeof(vectorHead) + sizeof(vectorTail) - 1); - Require(std::strcmp(vectorFirst, "atomic ") == 0 && std::strcmp(vectorSecond, "datagram") == 0); + Require(std::memcmp(vectorFirst, "atomic ", sizeof(vectorFirst)) == 0 && std::strcmp(vectorSecond, "datagram") == 0); int enabled = 1; Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); int option = 0; From baaad78e81d85214dc1738d750bfed9e96b06863 Mon Sep 17 00:00:00 2001 From: Cesar Rodriguez <11452991+Tamalero@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:32:34 -0700 Subject: [PATCH 025/353] fix(agc): leave blending off for a color target with CB_COLOR_INFO.BLEND_BYPASS DecodeState threw "blend bypass conflicts with enabled blending" when CB_BLENDn_CONTROL enabled blending for a target whose CB_COLORn_INFO has BLEND_BYPASS (bit 16, Mesa src/amd/registers/gfx10.json) set. The bit makes the color block skip the blender for that target: Mesa sets it "according to docs" for UINT/SINT and 8_24/24_8/X24_8_32_FLOAT targets, which the blender cannot process (src/amd/common/ac_descriptors.c, shared by radeonsi and RADV). Marvel's Guardians of the Galaxy (PPSA01748) issues such draws right after its loading screen. Blending is now enabled only when it is requested and not bypassed. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 3 +-- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f151a5f0bb..b2eb57f05f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -868,11 +868,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t component = 0; component < 4; ++component) { if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; } - state.blendEnable = (blend >> 30u) & 1u; + state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0; if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented"); Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported"); if (state.blendEnable) { - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); state.srcColorBlendFactor = blendFactor(blend & 0x1fu); state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu); state.colorBlendOp = blendOp((blend >> 5u) & 7u); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 22b5c62dd6..93505b5ed2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -963,6 +963,16 @@ void PixelInputLayoutTests() { Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded"); } +void BlendBypassTests() { + auto queue = makeState(); + for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; + queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u); + Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded"); + queue.context[0x31c] |= 0x10000u; + const auto bypassed = AgcDriver::Graphics::DecodeState(queue); + Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends"); +} + void OpaqueDestinationAlphaTests() { auto queue = makeState(); for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; @@ -5130,6 +5140,7 @@ int main(int argc, char** argv) { DisabledColorTests(); OpaqueDestinationAlphaTests(); Color1555TargetTests(); + BlendBypassTests(); CompactedExportTests(); ReversedComponentOrderTests(); ShadersWriteMemoryTests(); From c15e9f2a374366ace0cc2348d5b727ed31011466 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:26:26 -0400 Subject: [PATCH 026/353] fix(libSceVideoOut): return RESOURCE_BUSY when a bus is opened twice --- core/libs/prx/libSceVideoOut/src/Output.cpp | 4 +--- core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 2 +- core/libs/tests/GuestVideoOutOutputMode.cpp | 2 ++ docs/dev/debt/libSceVideoOut.md | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index f47aac9f3e..2a333efcc3 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -80,9 +80,7 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); } const int handle = VideoOutDriver::Get().Open(busType); - if (handle < 0) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY"); - } + if (handle < 0) return VIDEO_OUT_ERROR_RESOURCE_BUSY; return handle; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index f605f9dbb6..ed2e7538b4 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -336,7 +336,7 @@ int VideoOutDriver::Open(int busType) { uint64_t generation = 1; if (previous) { std::lock_guard cfgLock(previous->mutex); - require(!previous->opened, "port already open"); + if (previous->opened) return -1; require(previous->generation != std::numeric_limits::max(), "port generation overflow"); generation = previous->generation + 1; } diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index da708754d1..4bed080a11 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -21,6 +21,7 @@ static constexpr int MAIN_BUS = 0; static constexpr int INVALID_VALUE = static_cast(0x80290001); static constexpr int INVALID_ADDRESS = static_cast(0x80290002); static constexpr int INVALID_HANDLE = static_cast(0x8029000B); +static constexpr int RESOURCE_BUSY = static_cast(0x80290009); static constexpr int UNSUPPORTED_OUTPUT_MODE = static_cast(0x80290016); static constexpr int UNKNOWN_OUTPUT_MODE = static_cast(0x8029001E); @@ -50,6 +51,7 @@ int main() { return 77; } Require(handle > 0); + Require(sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr) == RESOURCE_BUSY); Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 3a7fc1e81f..1d7c81853c 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -20,4 +20,5 @@ ### Functional +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses From 126a1e4137276e540e34c507a95f18634912e42d Mon Sep 17 00:00:00 2001 From: TideOPL Date: Tue, 13 Oct 2026 01:29:21 +0100 Subject: [PATCH 027/353] perf(agc): copy a pending block-compressed surface into its sampled texture on the GPU A compute pass can write a block-compressed surface through an uncompressed storage view whose element is one block (32_32_UINT for 8-byte blocks, 32_32_32_32_UINT for 16-byte ones). cachedTexture rejected such a storage image for the BC descriptor, so every sample stored the whole surface to guest memory, compared it with the snapshot and uploaded it again. Texture::CanBlockCopyFrom accepts a storage image over the same memory, tiling and swizzle whose element is the block and whose extent is the surface's extent in blocks, with whole blocks on every copied level. The sampled texture then owns a BC image that vkCmdCopyImage refreshes from the storage image when its version moved, through the same refreshed-on-reuse path as the depth copy. --- .../Graphics/include/Texture.hpp | 5 + .../Graphics/src/ShaderResources.cpp | 11 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 115 +++++++++ .../tests/execution/BcBlockCopy.cpp | 244 ++++++++++++++++++ 4 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 6dafa8345a..ddc21909f1 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, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); + struct BlockCopyTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag); + static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; } bool RefreshDepthCopy(); struct DepthCopyCounts { @@ -106,6 +109,7 @@ public: private: void release() noexcept; + bool refreshBlockCopy(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); @@ -133,6 +137,7 @@ private: bool copiedOnce = false; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat blockFormat = VK_FORMAT_UNDEFINED; std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 8dc5166c5d..76e31fe546 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -553,7 +553,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; auto source = StorageTexture::FindPending(address, guestBytes); - if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); + if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -624,6 +625,11 @@ std::shared_ptr cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && blockCopy) { + auto texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -676,6 +682,9 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::DepthCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (blockCopy) { + entry.texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + 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 b4800d00c5..08679d8111 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -475,6 +475,75 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; } +bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + const auto& from = source.Descriptor(); + if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false; + if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false; + if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const auto width = std::max(descriptor.width >> level, 1u); + const auto height = std::max(descriptor.height >> level, 1u); + if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false; + } + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { + try { + Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture"); + blockFormat = SampledTextureFormat(context, descriptor.format); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = blockFormat; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + 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, &image), "vkCreateImage block copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory block copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = blockFormat; + viewFormat = blockFormat; + 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 block copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + } catch (...) { + release(); + throw; + } +} + Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { try { Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture"); @@ -537,6 +606,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& 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(); 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); @@ -608,6 +678,51 @@ bool Texture::RefreshDepthCopy() { return true; } +bool Texture::refreshBlockCopy() { + std::vector regions; + for (std::uint32_t level = 0; level < copyLevels; ++level) { + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; + region.dstSubresource = region.srcSubresource; + region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u}; + regions.push_back(region); + } + 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}; + 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); + context.Function("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + 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; +} + Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() { return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp new file mode 100644 index 0000000000..23eeb6dcf7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format32x2Uint = 62; +constexpr std::uint32_t Format32x4Uint = 75; +constexpr std::uint32_t FormatBc1Unorm = 169; +constexpr std::uint32_t FormatBc7Unorm = 181; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t Texels = 256; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; +}; + +struct Case { + std::string name; + Shape blocks; + Shape compressed; + std::span store; +}; + +alignas(256) constexpr std::array StoreQuadCode{ + 0x7e28020c, 0x7e2a0300, 0x7e2c020c, 0x7e2e0300, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StorePairCode{ + 0x7e28020c, 0x7e2a0300, 0x7e3c0300, 0x7e3e0280, 0xf0201308, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf09c0f08, 0x00820402, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, +}; + +std::span Input; +std::span Output; + +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, "bc block copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "bc block 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(const void* data, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + 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; + return ShaderRecompiler::Recompile(request); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Case& test, std::uint32_t value) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, test.blocks); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = value; + const auto result = Compile(device, test.store, userData); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(test.store.data())); + device.WaitIdle(); +} + +std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels, shape))).guestBytes; +} + +void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); +} + +std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape) { + std::vector userData(24, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); + const auto texture = TextureDescriptor(texels, shape); + const auto sampler = SamplerDescriptor(); + 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 auto program = Compile(device, SampleCode, userData); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = 0.0f; + Input[tid * 3u + 1u] = (static_cast(tid) * 4.0f + 1.5f) / static_cast(shape.width); + Input[tid * 3u + 2u] = 1.5f / static_cast(shape.height); + } + std::fill(Output.begin(), Output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value) { + GuestBlock block; + auto* texels = block.Data(); + Store(device, texels, test, value); + Settle(device, texels, SurfaceBytes(texels, test.blocks)); + return Sample(device, texels, test.compressed); +} + +void RequireSame(const std::vector& sampled, const std::vector& expected, const std::string& what) { + for (std::size_t word = 0; word < expected.size(); ++word) { + Require(sampled[word] == expected[word], what + ": thread " + std::to_string(word / 4u) + " component " + std::to_string(word % 4u) + " read " + std::to_string(sampled[word]) + ", expected " + std::to_string(expected[word])); + } +} + +void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { + constexpr std::uint32_t First = 0x13572440u; + constexpr std::uint32_t Second = 0x9bdf6840u; + const auto first = Reference(device, test, First); + const auto second = Reference(device, test, Second); + Require(first != second, test.name + ": the two block patterns sample alike"); + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, test.compressed); + Require(bytes == SurfaceBytes(texels, test.blocks), test.name + ": the block view and the compressed view disagree on the surface size"); + Store(device, texels, test, First); + Settle(device, texels, bytes); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of stored blocks"); + Store(device, texels, test, Second); + const auto pending = Sample(device, texels, test.compressed); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes) != nullptr, test.name + ": sampling the compressed view stored the pending blocks to guest memory"); + RequireSame(pending, second, test.name + ": sample of pending blocks"); + Store(device, texels, test, First); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of blocks stored again"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock buffers; + Input = std::span(reinterpret_cast(buffers.Data()), Threads * 3u); + Output = std::span(reinterpret_cast(buffers.Data() + 4096u), Threads * 4u); + PendingBlocksCase(*device, {"BC7 through 32x4 uint blocks", {Format32x4Uint, Texels / 4u, Texels / 4u}, {FormatBc7Unorm, Texels, Texels}, StoreQuadCode}); + PendingBlocksCase(*device, {"BC1 through 32x2 uint blocks", {Format32x2Uint, Texels / 4u, Texels / 4u}, {FormatBc1Unorm, Texels, Texels}, StorePairCode}); + std::puts("bc block copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 665e6f17822fd010d16005cb7f64bf02245462cb Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 02:40:04 +0400 Subject: [PATCH 028/353] fix(libkernel): fill every buffer of readv from a pipe on Windows --- core/libs/prx/libkernel/File/src/Stdio.cpp | 8 +++++++- core/libs/tests/GuestFileVector.cpp | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 71550eb1ca..2855e6194c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -218,6 +218,12 @@ static bool NativeIsDisk(int descriptor) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_DISK; } +static bool NativePipeHasData(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE + && ::PeekNamedPipe(handle, nullptr, 0, nullptr, &available, nullptr) && available != 0; +} static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { return NativePositioned_nid_no_patch(descriptor, buf, nbytes, offset, false); } @@ -1003,7 +1009,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno); done += result; position += static_cast(result); - if (static_cast(result) < chunk || !whole) return done; + if (static_cast(result) < chunk || (!whole && !NativePipeHasData(d))) return done; } } return done; diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 370be6be8f..eda9297d81 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -206,6 +206,15 @@ int main() { Require(sceKernelWritev(ends[1], message, 1) == 3); Require(sceKernelReadv(ends[0], reads, 2) == 3); Require(std::memcmp(first, "xyz", 3) == 0); + char stuvw[] = "stuvw"; + GuestIovec longer[1] = {{stuvw, 5}}; + Require(sceKernelWritev(ends[1], longer, 1) == 5); + Require(sceKernelReadv(ends[0], reads, 2) == 5); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vw", 2) == 0); + Require(sceKernelWritev(ends[1], longer, 1) == 5 && sceKernelWritev(ends[1], longer, 1) == 5); + Require(readv_nid_postfix(ends[0], reads, 2) == 7); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vwst", 4) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == 3 && std::memcmp(first, "uvw", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); #ifndef _WIN32 From 4ccf88829131f3f69f3eb50adbf78e3fa44c26cf Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:47:46 -0700 Subject: [PATCH 029/353] fix(savedata): leave the mount result untouched when sceSaveDataMount3 fails Animal Well (PPSA22520) checks for its save with a CREATE2 mount it never unmounts, then saves with a second mount of the same directory. That mount returns BUSY, and the title keeps using the first mount point for sceSaveDataPrepare, its save file and sceSaveDataUmount2. sceSaveDataMount3 cleared the result before its checks, so the save was written to "/save" in the game folder instead of the save directory, and the first mount was never released. The result is now written only when the mount succeeds, as shadPS4 does. --- .../libs/prx/libSceSaveData.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataMountStatus.cpp | 23 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 58e898833a..719a79dbfe 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -463,7 +463,6 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) { throw std::runtime_error("sceSaveDataMount3: null argument"); } - std::memset(mount_result, 0, sizeof(*mount_result)); const bool create = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; const bool create2 = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE2) != 0; const bool rdonly = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_RDONLY) != 0; @@ -498,6 +497,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (slot == -1) { return SAVE_DATA_ERROR_MOUNT_FULL; } + std::memset(mount_result, 0, sizeof(*mount_result)); if (create || create2) { std::filesystem::create_directories(real_path); } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index c51a1cb5c1..9249ab3838 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -18,6 +18,8 @@ namespace { constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; +constexpr int SaveDataErrorBusy = -2137063421; +constexpr int SaveDataErrorNotFound = -2137063416; int failures = 0; @@ -41,6 +43,26 @@ void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedS Check(sceSaveDataUmount2(0, &result.mount_point) == 0, what + ": unmounts"); } +void CheckFailedMountKeepsResult() { + SceSaveDataDirName held{}; + std::memcpy(held.data, "held", sizeof("held")); + SaveDataMount3 mount{}; + mount.dir_name = &held; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount3(&mount, &result) == 0, "first mount of a save"); + const SaveDataMountResult mounted = result; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "second mount of a mounted save is BUSY"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a BUSY mount leaves the result as it was"); + SceSaveDataDirName absent{}; + std::memcpy(absent.data, "absent", sizeof("absent")); + mount.dir_name = &absent; + mount.mount_mode = MountReadWrite; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorNotFound, "open of a missing save is NOT_FOUND"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a NOT_FOUND mount leaves the result as it was"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save"); +} + } int main() { @@ -53,6 +75,7 @@ int main() { MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save"); MountAndCheck("made", MountReadWrite, 0u, "open of an existing save"); + CheckFailedMountKeepsResult(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index dfec69c2a7..6f990d1754 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -12,4 +12,5 @@ ### Functional - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. +- [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` From 19150397ee245a72291bdce1cd31799304cc2c3f Mon Sep 17 00:00:00 2001 From: omurc4 Date: Mon, 12 Oct 2026 20:50:38 -0400 Subject: [PATCH 030/353] feat(relinker): hint at the MinGW runtime DLLs when a module fails to load with error 126 A relinked executable that cannot load a library reports "GetLastError: 126 - The specified module could not be found.", which also covers a library whose dependent DLLs are missing. On Windows a self-built libs/ directory needs libgcc_s_seh-1.dll, libstdc++-6.dll and libwinpthread-1.dll next to it. For error 126 the stub now adds a line naming them. Refs #4482 --- .../elfpatcher/src/windows/WindowsEntryStubBuilder.cpp | 6 ++++++ core/relinker/relinker/tests/test_windows_import_modules.py | 1 + 2 files changed, 7 insertions(+) diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index dcd704baba..20aef1c45f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -193,6 +193,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto failedModule = addString("Failed to load module: "); const auto skippedModule = addString(" -> not found; nothing is imported from it, continuing without it\n"); const auto errorPrefix = addString("GetLastError: "); + const auto moduleNotFoundHint = addString("hint: the module or one of its dependent DLLs was not found; a self-built libs/ also needs libgcc_s_seh-1.dll, libstdc++-6.dll and libwinpthread-1.dll from the toolchain's mingw64/bin next to it\n"); const auto messageSeparator = addString(" - "); const auto newline = addString("\n"); const auto searched = addString("Searched libraries:\n"); @@ -407,6 +408,11 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeString(resolvedPaths[index], true); writeString(newline, true); writeLastError(); + code.Rip({0x8b, 0x05}, lastError); + code.Emit({0x83, 0xf8, 0x7e}); + const auto notModuleNotFound = code.Branch({0x0f, 0x85}); + writeString(moduleNotFoundHint, true); + code.PatchBranch(notModuleNotFound, code.GetRva()); if (dependencyDiagnostics) { code.Rip({0x48, 0x8d, 0x0d}, resolvedPaths[index]); code.Rip({0x48, 0x8d, 0x15}, programText); diff --git a/core/relinker/relinker/tests/test_windows_import_modules.py b/core/relinker/relinker/tests/test_windows_import_modules.py index 5a03c9fa04..8d8d88e225 100644 --- a/core/relinker/relinker/tests/test_windows_import_modules.py +++ b/core/relinker/relinker/tests/test_windows_import_modules.py @@ -408,6 +408,7 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode != 0 and 'Failed to load module:' in run.stderr and 'missing.prx' in run.stderr, (run.returncode, run.stderr) assert run.stderr.startswith('Failed to load module:'), run.stderr + assert 'GetLastError: 126' in run.stderr and 'hint: the module or one of its dependent DLLs was not found' in run.stderr, run.stderr for index, (symbol, module_name) in enumerate([('shared#A#B', None), ('shared', None), ('shared#A#B', 'libSceJson')]): case = work / ('missing-unused-dependency-' + str(index)) From d6698c904598ac708158a128b7a627074eda66ac Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:43:34 -0700 Subject: [PATCH 031/353] refactor(agc): declare maxDrawSnapshotBytes after guestMemory #757 and #758 add a member after usesFaultBuffer, so declaring maxDrawSnapshotBytes there made the pull requests conflict. --- .../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 1d56dc3a45..b34edb385f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -431,10 +431,10 @@ private: Context context; std::vector layoutKey; GuestBufferMemory guestMemory; + std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; bool usesFaultBuffer = false; - std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); VkDescriptorSetLayout _layout = VK_NULL_HANDLE; // Whether the layout is this object's own (no cache) and destroyed with it. bool ownsLayout = false; From 3d550b3de4c95dccfc6bb01152fcb4ca3fe50119 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:58:04 -0700 Subject: [PATCH 032/353] fix(libc): return flush failures from fflush instead of throwing fflush threw when the host flush failed, which ends the title on an ordinary write error such as a full disk. FreeBSD's __sflush sets the stream's error flag and returns EOF, as C17 7.21.5.2 asks. fflush now returns the host result and keeps the guest stream's error flag in sync, as fwrite, fputs and fclose already do since #4380 and #3926. --- core/libs/prx/libc/src/FileAndHeap.cpp | 5 +++-- core/libs/tests/FileStreams.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 1c0899a120..2d3d9e4b6e 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -430,8 +430,9 @@ int APS5_VABI fflush_nid_postfix(FileStream* stream) { #ifdef __linux__ if (native && !__fwriting(native)) return 0; #endif - if (std::fflush(native) != 0) throw std::runtime_error("fflush: flush failed"); - return 0; + const int result = std::fflush(native); + if (stream) stream->SyncStatus(); + return result; } int APS5_VABI malloc_stats_fast_nid_postfix(void* stats) { diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index d0431836ba..393e233745 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -8,6 +8,12 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); @@ -39,6 +45,18 @@ static void ExpectException(TAction action) { throw std::runtime_error("Expected a stream exception"); } +static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) { +#ifdef _WIN32 + const int descriptor = _open(path, _O_RDONLY | _O_BINARY); + Require(descriptor >= 0 && _dup2(descriptor, _fileno(GetNativeStream(stream))) == 0); + Require(_close(descriptor) == 0); +#else + const int descriptor = open(path, O_RDONLY); + Require(descriptor >= 0 && dup2(descriptor, fileno(GetNativeStream(stream))) >= 0); + Require(close(descriptor) == 0); +#endif +} + int main(int argc, char** argv) { Require(argc == 2); Require(_Stdout_nid_postfix.GetHandle() == stdout); @@ -131,6 +149,12 @@ int main(int argc, char** argv) { Require(fputs_nid_postfix(marker, stream) == EOF); Require(ferror_nid_postfix(stream) != 0); Require(fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(argv[1], "wb"); + Require(stream != nullptr && fputs_nid_postfix(marker, stream) >= 0); + ReplaceWithReadOnlyDescriptor(stream, argv[1]); + Require(fflush_nid_postfix(stream) == EOF); + Require(ferror_nid_postfix(stream) != 0); + static_cast(fclose_nid_postfix(stream)); Require(std::remove(argv[1]) == 0); std::cout << "PASS: stream objects, file operations, EOF and error handling\n"; } From 17984f8c53695578b1f437598ff6f4d5b4fed659 Mon Sep 17 00:00:00 2001 From: TideOPL Date: Tue, 13 Oct 2026 04:02:14 +0100 Subject: [PATCH 033/353] perf(agc): start no collect epoch at a wait its own queue's stored label satisfies waitMemory starts a new collect epoch when it returns, so the next CollectWrites of every surface walks the write watch again. The label-table path already keeps the epoch for a non-late hit whose store came from the waiting queue: such a label orders only that queue's own work, no store by the guest or another queue. The two storedSince returns (at entry and while polling) answer the same question from the label-store history but kept the bump for a store of the waiting queue. They now keep the epoch in that case too. --- .../src/Driver/Synchronization/WaitMemory.cpp | 2 + .../tests/execution/SameQueueLabelWait.cpp | 164 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 9d15cf04bc..3deebcb869 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -140,6 +140,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (heldAtSubmit) return; if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; return; } @@ -332,6 +333,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que while (!Pm4::WaitSatisfiedUnchecked(packet)) { if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; return; } ++polls; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp new file mode 100644 index 0000000000..607faa3dd3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp @@ -0,0 +1,164 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t SpinIterations = 1u << 20u; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Waits = 16; + +struct Slot { + alignas(256) std::array spinOutput{}; + alignas(256) std::array spinDescriptor{}; + alignas(256) volatile std::uint32_t label = 0; + alignas(256) volatile std::uint32_t done = 0; +}; + +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, "same-queue label wait: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + Slot& Data() { return *reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void ReleaseLabel(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), Low(target), High(target), value, 0u, 0u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void WaitEqual(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(target), High(target), value, 0xffffffffu, 0x19u}); +} + +std::uint64_t Run(Slot& slot, std::uint32_t value, bool wait) { + slot.spinDescriptor = {Low(slot.spinOutput.data()), High(slot.spinOutput.data()) & 0xffffu, sizeof(slot.spinOutput), 0x31016facu}; + slot.done = 0; + std::vector words; + Dispatch(words, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterations}); + for (std::uint32_t index = 0; index < Waits; ++index) { + ReleaseLabel(words, &slot.label, value + index); + if (wait) WaitEqual(words, &slot.label, value + index); + } + WriteData(words, &slot.done, 1); + const auto before = AgcDriver::GuestMemory::CollectEpochBumps(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "the submission did not finish"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + AgcDriverWaitIdle_nid_postfix(); + Require(slot.label == value + Waits - 1, "the last label did not reach memory"); + return AgcDriver::GuestMemory::CollectEpochBumps() - before; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestBlock block; + auto& slot = block.Data(); + Run(slot, 1, false); + std::uint64_t extra = ~std::uint64_t{0}; + for (std::uint32_t attempt = 0; attempt < 3; ++attempt) { + const auto without = Run(slot, 100 + attempt * 2 * Waits, false); + const auto with = Run(slot, 100 + (attempt * 2 + 1) * Waits, true); + std::cout << "attempt " << attempt << ": " << with << " bumps with " << Waits << " waits, " << without << " without\n"; + extra = std::min(extra, with > without ? with - without : 0); + } + Require(extra < Waits / 2, "WAIT_REG_MEMs satisfied by labels their own queue released earlier in the submission started new collect epochs: " + std::to_string(extra) + " extra bumps for " + std::to_string(Waits) + " waits"); + std::cout << "same-queue label wait test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From f2120a645810ca02cb8589114b5cd5e562a7ce60 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:53:22 +0200 Subject: [PATCH 034/353] fix(libc): reject incompatible fdopen access modes on Windows --- .../prx/libc/include/WindowsDescriptor.hpp | 36 ++++++++++ core/libs/prx/libc/src/FileAndHeap.cpp | 6 ++ .../libkernel/File/src/DescriptorControl.cpp | 37 ++--------- core/libs/tests/GuestStandardStreams.cpp | 65 +++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 core/libs/prx/libc/include/WindowsDescriptor.hpp diff --git a/core/libs/prx/libc/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp new file mode 100644 index 0000000000..529011b509 --- /dev/null +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -0,0 +1,36 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP + +#include +#include +#include +#include +#include +#include + +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); + +static inline HANDLE WindowsDescriptorHandle_nid_no_patch(int descriptor) { + const auto ignore = +[](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}; + const auto previous = _set_thread_local_invalid_parameter_handler(ignore); + const auto handle = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +static inline int WindowsDescriptorAccess_nid_no_patch(HANDLE handle, int descriptor, const char* function) { + using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); + PUBLIC_OBJECT_BASIC_INFORMATION information{}; + const NTSTATUS status = query == nullptr ? -1 : query(handle, ObjectBasicInformation, &information, sizeof(information), nullptr); + if (status < 0) + throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(descriptor) + ", status=" + std::to_string(status)); + const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; + const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + if (read && write) return 2; + if (write) return 1; + if (read) return 0; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(descriptor) + " grants neither read nor write access"); +} + +#endif diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 1c0899a120..f0606de3af 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -19,6 +19,7 @@ #include #include #include +#include "prx/libc/include/WindowsDescriptor.hpp" #endif #include "prx/libc/include/FileStream.hpp" @@ -221,6 +222,11 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { const auto release = [](void* pointer) { ::operator delete(pointer); }; std::unique_ptr storage(::operator new(sizeof(FileStream)), release); #ifdef _WIN32 + const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); + if (handle == INVALID_HANDLE_VALUE) { errno = 9; return nullptr; } + const int access = WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "fdopen"); + const int requested = nativeMode.find('+') != std::string::npos ? 2 : nativeMode[0] == 'r' ? 0 : 1; + if (access != 2 && access != requested) { errno = 22; return nullptr; } if (parsed->append && !AppendThroughDescriptor(descriptor)) return nullptr; auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 5fa3171efb..bdbb35ed00 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -21,7 +21,7 @@ #ifdef _WIN32 #include #include -#include +#include "prx/libc/include/WindowsDescriptor.hpp" #else #include #include @@ -31,9 +31,6 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); -#ifdef _WIN32 -extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); -#endif namespace { @@ -137,20 +134,11 @@ const char* KindName(Kind kind) { } #ifdef _WIN32 -void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} - -HANDLE NativeHandle(int fd) { - const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); - const auto handle = ::_get_osfhandle(fd); - _set_thread_local_invalid_parameter_handler(previous); - return reinterpret_cast(handle); -} - std::optional Classify(int fd, const char* function) { if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; if (File::IsRandomDevice(fd)) return Kind::RandomDevice; - switch (::GetFileType(NativeHandle(fd))) { + switch (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd))) { case FILE_TYPE_DISK: return Kind::File; case FILE_TYPE_PIPE: return Kind::Pipe; case FILE_TYPE_CHAR: return Kind::Device; @@ -159,36 +147,25 @@ std::optional Classify(int fd, const char* function) { } int AccessMode(int fd, const char* function) { - using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); - static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); - PUBLIC_OBJECT_BASIC_INFORMATION information{}; - const NTSTATUS status = query == nullptr ? -1 : query(NativeHandle(fd), ObjectBasicInformation, &information, sizeof(information), nullptr); - if (status < 0) - throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(fd) + ", status=" + std::to_string(status)); - const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; - const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; - if (read && write) return ReadWrite; - if (write) return WriteOnly; - if (read) return ReadOnly; - throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " grants neither read nor write access"); + return WindowsDescriptorAccess_nid_no_patch(WindowsDescriptorHandle_nid_no_patch(fd), fd, function); } DWORD PipeState(int fd, const char* function) { DWORD state = 0; - if (!::GetNamedPipeHandleStateW(NativeHandle(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + if (!::GetNamedPipeHandleStateW(WindowsDescriptorHandle_nid_no_patch(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); return state; } void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); - if (!::SetNamedPipeHandleState(NativeHandle(fd), &mode, nullptr, nullptr)) + if (!::SetNamedPipeHandleState(WindowsDescriptorHandle_nid_no_patch(fd), &mode, nullptr, nullptr)) throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); } Entry InitialEntry(int fd) { int status = 0; - if (::GetFileType(NativeHandle(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + if (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; return {false, std::make_shared(status)}; } @@ -208,7 +185,7 @@ int Readable(int fd, Kind kind, const char* function) { } if (AccessMode(fd, function) == WriteOnly) return 0; DWORD available = 0; - if (!::PeekNamedPipe(NativeHandle(fd), nullptr, 0, nullptr, &available, nullptr)) { + if (!::PeekNamedPipe(WindowsDescriptorHandle_nid_no_patch(fd), nullptr, 0, nullptr, &available, nullptr)) { const DWORD error = ::GetLastError(); if (error == ERROR_BROKEN_PIPE) return 0; throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 4a214f9d61..7ae0fe680b 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -417,7 +418,71 @@ static void CheckDescriptorAppend(std::FILE* original) { } } +static void CheckDescriptorAccessModes() { + const auto filename = "anyps5-fdopen-modes-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + for (const int access : {0, 1, 2}) { + for (const char* mode : {"r", "w", "a", "r+", "w+", "a+"}) { + { std::ofstream output(filename); output << "retained"; } +#ifdef _WIN32 + const int descriptor = _open(filename.c_str(), access | _O_BINARY); +#else + const int descriptor = ::open(filename.c_str(), access); +#endif + Require(descriptor >= 0); + const int requested = std::strchr(mode, '+') ? 2 : *mode == 'r' ? 0 : 1; + const bool incompatible = access != 2 && access != requested; +#ifdef _WIN32 + const auto originalHandle = _get_osfhandle(descriptor); + if (incompatible) Require(_lseek(descriptor, 2, SEEK_SET) == 2); +#else + const int originalFlags = ::fcntl(descriptor, F_GETFL); + Require(originalFlags >= 0); + if (incompatible) Require(::lseek(descriptor, 2, SEEK_SET) == 2); +#endif + *__error_nid_postfix() = 0; + auto* stream = fdopen_nid_postfix(descriptor, mode); + if (incompatible) { + if (stream != nullptr || *__error_nid_postfix() != 22) + std::fprintf(stderr, "fdopen access=%d mode=%s returned %p with errno=%d\n", access, mode, + static_cast(stream), *__error_nid_postfix()); + Require(stream == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_get_osfhandle(descriptor) == originalHandle && _lseek(descriptor, 0, SEEK_CUR) == 2); +#else + Require(::fcntl(descriptor, F_GETFL) == originalFlags && ::lseek(descriptor, 0, SEEK_CUR) == 2); +#endif + Require(CheckFileBytes(filename, "retained", mode, false)); + const bool probeAppend = access == 1 && *mode == 'a'; +#ifdef _WIN32 + if (probeAppend) Require(_write(descriptor, "X", 1) == 1 && _lseek(descriptor, 0, SEEK_CUR) == 3); + Require(_close(descriptor) == 0); +#else + if (probeAppend) Require(::write(descriptor, "X", 1) == 1 && ::lseek(descriptor, 0, SEEK_CUR) == 3); + Require(::close(descriptor) == 0); +#endif + Require(CheckFileBytes(filename, probeAppend ? "reXained" : "retained", mode, false)); + } else { + Require(stream != nullptr); + if (requested == 0) Require(fgetc_nid_postfix(stream) == 'r'); + else Require(fputc_nid_postfix('X', stream) == 'X'); + Require(fclose_nid_postfix(stream) == 0); + } + } + } +#ifdef _WIN32 + const int closed = _open(filename.c_str(), _O_RDONLY | _O_BINARY); + Require(closed >= 0 && _close(closed) == 0); +#else + const int closed = ::open(filename.c_str(), O_RDONLY); + Require(closed >= 0 && ::close(closed) == 0); +#endif + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(closed, "r") == nullptr && *__error_nid_postfix() == 9); + Require(std::filesystem::remove(filename)); +} + int main() { + CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 598078c1f9..d70c39fb3b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,7 @@ ### Unknown function info +- [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. From 7cba4ce60ab441ca33c1b725fc3818cb9199a9b9 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 10:25:31 -0700 Subject: [PATCH 035/353] fix(libkernel_sys): let link follow a symbolic link like FreeBSD link threw "a symbolic link as the existing path is not supported" when its first argument named a symbolic link, so a valid call aborted the title. FreeBSD's link follows that link (sys/kern/vfs_syscalls.c, sys_link: "kern_linkat(td, AT_FDCWD, AT_FDCWD, uap->path, uap->link, UIO_USERSPACE, FOLLOW)") and hard-links the file it points to; a link to a directory then fails with EPERM ("if (vp->v_type == VDIR) ... return (EPERM);") and a dangling one with ENOENT from namei. link now checks the followed status of the existing path and, when it is a symbolic link, creates the hard link to the file it resolves to. std::filesystem::create_hard_link would otherwise link the symbolic link itself on Linux. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit a521101b77c879234308711c5ec8a9f9cf00816f) --- core/libs/prx/libkernel_sys/Export.cpp | 6 +++--- core/libs/tests/GuestKernelSys.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index fee23a9188..81cd9b2d0b 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -264,13 +264,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); std::error_code error; - const auto status = std::filesystem::symlink_status(source, error); + const auto status = std::filesystem::status(source, error); if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); - if (std::filesystem::is_symlink(status)) throw std::runtime_error("link: a symbolic link as the existing path is not supported"); if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(GuestEnoent)); - std::filesystem::create_hard_link(source, destination, error); + const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; + if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); if (error) throw std::system_error(error, "link"); RecordWrittenPath_nid_no_patch(destination); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 8f5a1b913f..7d7484e79b 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -297,6 +297,20 @@ int main() { Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); Require(sceKernelClose(base) == 0); +#ifndef _WIN32 + if (created == 0) { + const auto throughLink = (root / "through-link.bin").string(); + Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); + Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); + Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); + Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); + Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); + Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); + } +#endif std::filesystem::remove_all(root); return 0; } From 2f1eb100a03a0782bf6dba912936f371151e1a1b Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 06:59:08 +0400 Subject: [PATCH 036/353] fix(libkernel): fail reads on a directory descriptor with EISDIR on Windows (cherry picked from commit d7ef271ef7eeb1d82a337a0b0a9767ce68721bd4) --- core/libs/prx/libkernel/File/src/Open.cpp | 4 ++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 3 ++- core/libs/tests/GuestFilesystem.cpp | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 2a1edd0183..1155960dd9 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -53,6 +53,10 @@ static int NativeRead(int fd, void* buf, std::size_t n) { } const int result = ::_read(fd, buf, static_cast(n)); if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; + if (result == 0 && File::DirectoryDescriptorPath(fd)) { + errno = EISDIR; + return -1; + } return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f422485031..58e25ea3d4 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -184,7 +184,7 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ } LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { - errno = ESPIPE; + errno = !File::DirectoryDescriptorPath(descriptor) ? ESPIPE : write ? EBADF : EISDIR; return -1; } OVERLAPPED overlapped{}; @@ -991,6 +991,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write); return result < 0 ? SceErrorFromErrno(errno) : 0; } + if (!write && offset == nullptr && File::DirectoryDescriptorPath(d)) return SceErrorFromErrno(GUEST_EISDIR); const bool whole = write || offset != nullptr || NativeIsDisk(d); std::int64_t done = 0; for (int i = 0; i < iovcnt; ++i) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index d32843235b..18822eeb2b 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -51,6 +51,11 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelOpen(const char*, int, unsigned short); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +std::int64_t APS5_VABI sceKernelReadv(int, const void*, int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPreadv(int, const void*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelStat(const char*, FileStat*); int APS5_VABI sceKernelCheckReachability(const char*); int APS5_VABI sceKernelFstat(int, FileStat*); @@ -602,7 +607,18 @@ int main() { Require(syncedLocked >= 0 && sceKernelFsync(syncedLocked) == 0 && close_nid_postfix(syncedLocked) == 0); Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0600) == 0); const int directoryDescriptor = open_nid_postfix(rootName.c_str(), 0, 0); - Require(directoryDescriptor >= 0 && close_nid_postfix(directoryDescriptor) == 0); + Require(directoryDescriptor >= 0); + char directoryBytes[8]{}; + struct { void* base; std::size_t length; } directoryVector[2] = {{directoryBytes, 3}, {directoryBytes + 3, 5}}; + Require(read_nid_postfix(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == -1 && *__error_nid_postfix() == 21); + Require(sceKernelRead(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == static_cast(0x80020015u)); + Require(sceKernelReadv(directoryDescriptor, directoryVector, 2) == static_cast(0x80020015u)); + Require(sceKernelPread(directoryDescriptor, directoryBytes, sizeof(directoryBytes), 0) == static_cast(0x80020015u)); + Require(sceKernelPreadv(directoryDescriptor, directoryVector, 2, 0) == static_cast(0x80020015u)); + Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); + std::vector listing(4096); + Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); + Require(close_nid_postfix(directoryDescriptor) == 0); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); Require(flaggedDirectory >= 0 && sceKernelClose(flaggedDirectory) == 0); std::vector entries(4096); From 097bc538ee1525e140560ad6c6842f69a738cc68 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 07:03:05 +0400 Subject: [PATCH 037/353] fix(libkernel): fail ftruncate on a pipe, a directory and a device with EINVAL on Windows (cherry picked from commit 971fb51ef3f09d6543ef20e05e4a44572d2aadbd) --- core/libs/prx/libkernel/File/src/Stdio.cpp | 5 ++++- core/libs/tests/GuestFilesystem.cpp | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f422485031..ecd959794e 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -105,7 +105,10 @@ static int NativeFchmod(int descriptor, int mode) { } static int NativeFtruncate(int descriptor, std::int64_t length) { const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); - return result == -2 ? static_cast(::_chsize_s(descriptor, length)) : result; + if (result != -2) return result; + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; + return static_cast(::_chsize_s(descriptor, length)); } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index d32843235b..cefe2363c6 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -226,6 +226,20 @@ int main() { Require(created >= 0 && close_nid_postfix(created) == 0 && std::filesystem::file_size("creat_probe.bin") == 0); Require(std::filesystem::remove("creat_probe.bin")); } + { + int ends[2] = {-1, -1}; + Require(pipe_nid_postfix(ends) == 0 && write_nid_postfix(ends[1], "abc", 3) == 3); + for (const int end : ends) { + Require(sceKernelFtruncate(end, 0) == static_cast(0x80020016u) && sceKernelFtruncate(end, 8) == static_cast(0x80020016u)); + } + char kept[4]{}; + Require(read_nid_postfix(ends[0], kept, sizeof(kept)) == 3 && std::memcmp(kept, "abc", 3) == 0); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + for (const char* name : {".", "/dev/urandom"}) { + const int opened = open_nid_postfix(name, 0, 0); + Require(opened >= 0 && sceKernelFtruncate(opened, 0) == static_cast(0x80020016u) && close_nid_postfix(opened) == 0); + } + } Require(close_nid_postfix(descriptors[0]) == 0); auto* hostCapture = std::tmpfile(); Require(hostCapture != nullptr && pipe_nid_postfix(descriptors) == 0); From c1bcd0b6973a57028dadcc3512816f53a5c8a02b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 05:05:31 +0000 Subject: [PATCH 038/353] fix(videoout): dump linear display buffers --- .../Execution/src/VulkanDevice.cpp | 17 ++++- .../libSceVideoOut/tests/FrameDumpQueue.cpp | 67 +++++++++++++------ 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4153efa148..21eb476fae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2556,9 +2556,23 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); + auto& dumps = Dumps(); if (buffer.tilingMode == 1) { const auto pixels = ReadDisplayBuffer(buffer); - return present(buffer.width, buffer.height, true, pixels); + bool dumpFrame = false; + if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (dumps.cpu) { + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale()); + } else { + state->nextDumpIndex = dumps.dumped++; + dumpFrame = true; + } + } + if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) { + if (dumpFrame) --dumps.dumped; + return false; + } + return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; // The display buffer is usually a resident render target whose results are still on the GPU: it @@ -2597,7 +2611,6 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; - auto& dumps = Dumps(); bool dumpFrame = false; if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp index 2c5b74ad1d..a5105da79f 100644 --- a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -96,31 +96,52 @@ void checkDump(std::span dump, std::span decoded) { void testDumpQueue() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); - std::vector allocation(static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535); - const auto address = reinterpret_cast(allocation.data()); - const std::span storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u}; - const AgcDriver::DisplayBuffer display{reinterpret_cast(storage.data()), 0x8000000000000000ull, Width, Height}; - check(AgcDriver::DisplayBufferSize(display) == storage.size(), "incorrect padded display footprint"); - for (std::uint32_t y = 0; y < Height; ++y) { - for (std::uint32_t x = 0; x < Width; ++x) { - const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); - storage[offset] = static_cast(x & 255u); - storage[offset + 1] = static_cast(y & 255u); - storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); - storage[offset + 3] = std::byte{255}; + constexpr auto tiledBytes = static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536; + constexpr auto linearBytes = static_cast(Width) * Height * 4; + const auto makeStorage = [](std::size_t bytes, std::vector& allocation) { + allocation.resize(bytes + 65535); + const auto address = reinterpret_cast(allocation.data()); + auto* aligned = allocation.data() + ((65536u - (address & 65535u)) & 65535u); + return std::span{aligned, bytes}; + }; + std::vector tiledAllocation; + std::vector linearAllocation; + const auto tiledStorage = makeStorage(tiledBytes, tiledAllocation); + const auto linearStorage = makeStorage(linearBytes, linearAllocation); + const AgcDriver::DisplayBuffer tiled{reinterpret_cast(tiledStorage.data()), 0x8000000000000000ull, Width, Height}; + const AgcDriver::DisplayBuffer linear{reinterpret_cast(linearStorage.data()), 0x8000000000000000ull, Width, Height, 1}; + check(AgcDriver::DisplayBufferSize(tiled) == tiledStorage.size(), "incorrect padded tiled display footprint"); + check(AgcDriver::DisplayBufferSize(linear) == linearStorage.size(), "incorrect linear display footprint"); + const auto fill = [](const AgcDriver::DisplayBuffer& display, std::span storage) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); + storage[offset] = static_cast(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); + storage[offset + 3] = std::byte{255}; + } } - } - const auto decoded = AgcDriver::ReadDisplayBuffer(display); - VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; - VideoOutBufferAttribute2 attribute{}; - sceVideoOutSetBufferAttribute2(&attribute, display.pixelFormat, 0, Width, Height, 0, 0, 0); - check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + }; + fill(tiled, tiledStorage); + fill(linear, linearStorage); + const auto tiledPixels = AgcDriver::ReadDisplayBuffer(tiled); + const auto linearPixels = AgcDriver::ReadDisplayBuffer(linear); + VideoOutBuffers tiledBuffer{tiledStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBuffers linearBuffer{linearStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 tiledAttribute{}; + VideoOutBufferAttribute2 linearAttribute{}; + sceVideoOutSetBufferAttribute2(&tiledAttribute, tiled.pixelFormat, 0, Width, Height, 0, 0, 0); + sceVideoOutSetBufferAttribute2(&linearAttribute, linear.pixelFormat, 1, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &tiledBuffer, 1, &tiledAttribute, 0, nullptr) == 0, "tiled buffer registration failed"); + check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); const HeldDump first(0); const HeldDump last(Frames - 1); std::exception_ptr failure; try { for (int frame = 0; frame < Frames; ++frame) { - check(sceVideoOutSubmitFlip(handle, 0, 1, frame) == 0, "the flip was not accepted"); + const int index = frame % 2; + check(sceVideoOutSubmitFlip(handle, index, 1, frame) == 0, "the flip was not accepted"); std::unique_lock lock(cfg->mutex); check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast(frame) + 1; }), "the flip did not complete"); if (cfg->failure) std::rethrow_exception(cfg->failure); @@ -130,11 +151,13 @@ void testDumpQueue() { } const auto firstDump = first.Release(); if (failure) std::rethrow_exception(failure); - checkDump(firstDump, decoded); + checkDump(firstDump, tiledPixels); const auto lastDump = last.Release(); - checkDump(lastDump, decoded); - for (int frame = 1; frame < Frames - 1; ++frame) checkDump(readDump(frame), decoded); + checkDump(lastDump, linearPixels); + checkDump(readDump(1), linearPixels); + checkDump(readDump(2), tiledPixels); sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutUnregisterBuffers(handle, 1); sceVideoOutClose(handle); LibcRunShutdown_nid_postfix(); } From d39cb2c62f9add7622b26165593e2a344473c9ce Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:34:18 -0400 Subject: [PATCH 039/353] feat(libkernel): report machdep.tsc_freq from sysctlbyname --- .../System/src/SystemConfiguration.cpp | 9 +++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 27 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 672c9d2911..6dce175f2f 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -20,6 +20,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI getpid_nid_postfix(); +extern "C" std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -210,6 +211,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr std::uint32_t sdkVersion = 0x01000000; return CopyOut(&sdkVersion, sizeof(sdkVersion), oldValue, oldLength); } + if (std::strcmp(name, "machdep.tsc_freq") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::uint64_t frequency = sceKernelGetTscFrequency(); + return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); + } if (std::strcmp(name, "hw.ncpu") == 0) { static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 1ad0f5c6f7..3c767fa1b1 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -22,6 +22,7 @@ int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); int APS5_VABI getpid_nid_postfix(); +std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -106,6 +107,21 @@ static void CheckModelSysctl() { Require(sysctlbyname_nid_postfix("hw.model", model, &length, model, size) == -1 && *__error_nid_postfix() == 1); } #endif +static void CheckTscFrequencySysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, nullptr, 0) == 0); + Require(value == sceKernelGetTscFrequency() && value != 0 && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + std::uint32_t truncated = 0; + length = sizeof(truncated); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} static void CheckMachdepCachelineSizeSysctl() { *__error_nid_postfix() = 13; int value = 0x5a5a5a5a; @@ -255,6 +271,7 @@ int main() { #if defined(__x86_64__) || defined(__i386__) CheckModelSysctl(); #endif + CheckTscFrequencySysctl(); CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); CheckSystemSwVersion(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index edd23403ca..e228d395c7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -130,6 +130,7 @@ - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host CPU's name, built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) +- [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. From abc997b0daf691918aa8ae681debdc10c43328d4 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 00:07:29 +0000 Subject: [PATCH 040/353] fix(agc): release cached address space on device teardown --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++- .../Graphics/src/GuestBufferMemory.cpp | 5 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 33 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4b1fc3d984..168bcd3b96 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1280,8 +1280,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) + add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown) + set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(NOT WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index ec5d3a4f55..878fa0698e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1784,6 +1784,10 @@ MirrorStats MirrorCounters() { } void ClearImageMirrors(VkDevice device) { + auto space = Spaces().current.load(); + if (space != nullptr && space->device == device) { + Spaces().current.compare_exchange_strong(space, std::shared_ptr{}); + } auto& state = Mirrors(); std::map> entries; { @@ -1794,7 +1798,6 @@ void ClearImageMirrors(VkDevice device) { state.heapBytes = 0; state.device = VK_NULL_HANDLE; } - Spaces().current.store(nullptr); } void ClearHostImports(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7bfbc5a0d1..4e219ae0a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" @@ -22,6 +23,7 @@ #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include #include #include @@ -2725,6 +2727,32 @@ AgcDriver::Graphics::Context mockContext() { return context; } +void addressSpaceTeardownTests() { +#ifdef _WIN32 + _putenv_s("APS5_NO_LEASE_MIRROR", "1"); +#else + setenv("APS5_NO_LEASE_MIRROR", "1", 1); +#endif + auto context = mockContext(); + context.device = makeHandle(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64); + const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops; + { + AgcDriver::Graphics::GuestBufferMemory memory(context); + memory.AcquireRegistered(); + Require(memory.HoldsLease(), "address-based build holds no lease before device teardown"); + memory.Upload(true); + memory.WriteBack(); + Require(!memory.HoldsLease(), "completed address-based build kept its own lease"); + } + AgcDriver::Graphics::ClearImageMirrors(context.device); + GuestHeap::GuestHeapFree_nid_postfix(block); + Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations"); + Require(mock.live == 0, "address-space teardown test leaked Vulkan objects"); +} + using Role = ShaderRecompiler::DescriptorRole; using Kind = ShaderRecompiler::DescriptorKind; @@ -5317,6 +5345,11 @@ int main(int argc, char** argv) { std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n'; return passed ? 0 : 1; } + if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { + addressSpaceTeardownTests(); + std::cout << "address-space device teardown releases cached guest allocations\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; From f745a00a5d268c334a3953e31e1bf3647dee84f7 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 07:46:49 +0200 Subject: [PATCH 041/353] fix(libc): accept the full llroundl integer range --- core/libs/prx/libc/src/Math.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index fdef084eab..07da1655a4 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -18,7 +18,7 @@ namespace { template std::int64_t RoundToInt64(TFloat value) { const TFloat rounded = std::round(value); - if (!(rounded >= static_cast(INT64_MIN) && rounded < static_cast(INT64_MAX))) { + if (!(rounded >= static_cast(INT64_MIN) && rounded < -static_cast(INT64_MIN))) { std::feraiseexcept(FE_INVALID); return INT64_MIN; } From 2c23ced70e3dd80f565a3a7db9f007221ea615b1 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 07:47:45 +0200 Subject: [PATCH 042/353] test(libc): cover llroundl boundaries in every rounding mode --- core/libs/tests/GuestMath.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index f3089541b8..1974bf0885 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -599,6 +599,17 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) } } +static void CheckLongDoubleRounding(long double value, std::int64_t expected, bool invalid) { + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const std::int64_t result = llroundl_nid_postfix(value); + if (result != expected || (std::fetestexcept(FE_INVALID) != 0) != invalid || errno != 12345) { + std::fprintf(stderr, "llroundl failed for %La: expected %lld, got %lld\n", value, + static_cast(expected), static_cast(result)); + std::abort(); + } +} + static void CheckNoConversionIsPositiveZero() { for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; @@ -718,6 +729,21 @@ int main() { Require(std::abs(lgamma_nid_postfix(-0.5) - 1.2655121234846454) < 1e-15); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); + for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + CheckLongDoubleRounding(9223372036854775807.0L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.5L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.0L, INT64_MAX - 1, false); + CheckLongDoubleRounding(-0x1p63L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.5L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.0L, INT64_MIN + 1, false); + for (const long double value : {9223372036854775807.5L, 0x1p63L, -9223372036854775809.0L, + std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + CheckLongDoubleRounding(value, INT64_MIN, true); + } + } + Require(std::fesetround(FE_TONEAREST) == 0); Require(logbf_nid_postfix(8.f) == 3.f && logbf_nid_postfix(-0.75f) == -1.f); Require(logbf_nid_postfix(std::numeric_limits::denorm_min()) == -149.f); Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); From 2a91815210218417b48d1a350d2b8060701a7696 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 05:52:46 +0000 Subject: [PATCH 043/353] fix(tools): parse paginated overlap API responses --- tools/pr_overlap.py | 19 ++++++++++++++++++- tools/tests/test_pr_overlap.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 81ba717817..f059c7a989 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -40,7 +40,24 @@ def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") prs = {} - for pr in json.loads(out): + decoder = json.JSONDecoder() + offset = 0 + page_count = 0 + pull_requests = [] + while offset < len(out): + while offset < len(out) and out[offset].isspace(): + offset += 1 + if offset == len(out): + break + page, offset = decoder.raw_decode(out, offset) + if not isinstance(page, list): + raise ValueError("gh api --paginate returned a non-array page") + page_count += 1 + pull_requests.extend(page) + if page_count == 0: + raise ValueError("gh api --paginate returned no JSON pages") + + for pr in pull_requests: prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", "sha": pr["head"]["sha"], diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index f250ca15d9..55d0f4319b 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -54,6 +54,28 @@ class OpenPullRequestsTests(unittest.TestCase): 7: {"ref": "refs/pr/7", "sha": "head-sha", "author": "contributor", "depends": set()}, }) + def test_collects_concatenated_json_pages_from_paginated_gh_output(self): + pull_requests = [ + {"number": number, "user": {"login": f"author{number}"}, + "head": {"sha": f"sha{number}"}, "body": None} + for number in range(1, 4) + ] + pages = [pull_requests[:2], pull_requests[2:]] + output = "\n".join(json.dumps(page) for page in pages) + + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "gh", return_value=output): + found = pr_overlap.open_prs("main") + + self.assertEqual(set(found), {1, 2, 3}) + self.assertEqual(found[3]["sha"], "sha3") + + def test_rejects_empty_paginated_output(self): + with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ + patch.object(pr_overlap, "gh", return_value=" \n"): + with self.assertRaisesRegex(ValueError, "returned no JSON pages"): + pr_overlap.open_prs("main") + class FetchTests(unittest.TestCase): def test_fetches_each_head_commit_the_api_reported(self): From 66edc7463f927f1876e716c6d37c1d4c14a0f622 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:59:10 +0200 Subject: [PATCH 044/353] fix(tools): audit stubs from shared library sources --- tools/import_audit.py | 13 ++++++++ tools/tests/test_import_audit.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tools/import_audit.py b/tools/import_audit.py index 276ce39f84..c50d119e56 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -204,17 +204,30 @@ def source_stubs(source): if not Path(source).is_dir(): raise AuditError(f"{source}: not a directory") stubs = {} + shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in library.rglob("*.cpp"): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) + if "shared_sources" in group: + shared[library.name] = group["shared_sources"] done = {nid_of(name, aliases) for name in group["done_names"]} for name in group["todo_names"]: nid = nid_of(name, aliases) if nid not in done: stubs.setdefault((library.name + ".prx", nid), name) + for library, owner in shared.items(): + chain = [library] + while owner in shared: + if owner in chain: + raise AuditError(f"{source}: shared source cycle: {' -> '.join(chain + [owner])}") + chain.append(owner) + owner = shared[owner] + for (provider, nid), name in list(stubs.items()): + if provider == owner + ".prx": + stubs[(library + ".prx", nid)] = name return stubs diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 5534297e02..b812c64e2d 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -191,6 +191,57 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") + def test_cmake_shared_library_preserves_stub_names_and_implemented_exports(self): + work = self.work + owner, library = "libZOwner", "libAOwner.native" + pending, runtime, live = "scePending", "printf_nid_postfix", "sceLive" + alias_nid, alias_name = "AAAAAAAAAAA", "sceOwnerUnknown" + write_source(work.source, owner, STUB_SOURCE.format(name=pending) + STUB_SOURCE.format(name=runtime) + + f'APS5_EXPORT("{alias_nid}", {alias_name});\n' + STUB_SOURCE.format(name=alias_name) + + DONE_SOURCE.format(name=live)) + shared = work.source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + expected = {nid_names.compute_nid(pending): pending, nid_names.compute_nid("printf"): runtime, alias_nid: alias_name} + live_nid = nid_names.compute_nid(live) + (work.libs / (library + ".prx")).write_bytes(exporting(*expected, live_nid)) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [(nid, library + ".prx") for nid in [*expected, live_nid]]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + records = {record["nid"]: record for record in json.loads(result.read_text())["imports"]} + self.assertEqual({nid: (records[nid]["class"], records[nid]["name"]) for nid in expected}, + {nid: ("stub", name) for nid, name in expected.items()}) + self.assertEqual((records[live_nid]["class"], records[live_nid]["name"]), ("implemented", None)) + + def test_source_include_shared_library_preserves_stubs(self): + source = self.work.source + write_source(source, "libOwner", STUB_SOURCE.format(name="scePending")) + write_source(source, "libShared", '#include "prx/libOwner/Export.cpp"\n') + nid = nid_names.compute_nid("scePending") + self.assertEqual(import_audit.source_stubs(source), { + ("libOwner.prx", nid): "scePending", ("libShared.prx", nid): "scePending"}) + + def test_transitive_shared_library_preserves_stubs(self): + source = self.work.source + write_source(source, "libZOwner", STUB_SOURCE.format(name="scePending")) + for library, owner in (("libA", "libB"), ("libB", "libZOwner")): + shared = source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + nid = nid_names.compute_nid("scePending") + self.assertEqual(import_audit.source_stubs(source), { + (library + ".prx", nid): "scePending" for library in ("libA", "libB", "libZOwner")}) + + def test_shared_source_cycle_fails(self): + source = self.work.source + for library, owner in (("libA", "libB"), ("libB", "libA")): + shared = source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + with self.assertRaisesRegex(import_audit.AuditError, "shared source cycle.*libA.*libB"): + import_audit.source_stubs(source) + def test_formatted_export_aliases_remain_stubs(self): work = self.work nid = "AAAAAAAAAAA" From c7aca86c61b3bcb7dcfdc8604330cddfff5edb51 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 06:01:10 +0000 Subject: [PATCH 045/353] fix(tools): use the shared merge base for overlap checks --- tools/pr_overlap.py | 2 +- tools/tests/test_pr_overlap.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index f059c7a989..2a5b0d29fe 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -139,7 +139,7 @@ def clashes(a, b, path): def conflicts(a, b): if not (a["tree"] and b["tree"] and a["files"] & b["files"]): return {} - files = merge("--merge-base=refs/pr/base", a["tree"], b["tree"])[1] + files = merge(a["tree"], b["tree"])[1] return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 55d0f4319b..ed4f4e4b25 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -108,5 +108,14 @@ class FetchTests(unittest.TestCase): self.assertEqual(git.call_args_list[-1].args[-1], "+00000000000000000000000000000000000000f9:refs/pr/249") +class ConflictTests(unittest.TestCase): + def test_uses_the_shared_base_parent_without_newer_merge_tree_option(self): + first = {"tree": "first-tree", "files": {"shared.cpp"}, "added": set()} + second = {"tree": "second-tree", "files": {"shared.cpp"}, "added": set()} + with patch.object(pr_overlap, "merge", return_value=("merged-tree", set())) as merge: + self.assertEqual(pr_overlap.conflicts(first, second), {}) + merge.assert_called_once_with("first-tree", "second-tree") + + if __name__ == "__main__": unittest.main() From 5339c084e8fbea71244afb3e046ebfae16b9097f Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:02:12 +0200 Subject: [PATCH 046/353] perf(libkernel): cache the unwind tables and image name per module on Windows Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 52 +++++++++++++-- core/libs/prx/libkernel/Module/src/Module.cpp | 66 ++++++++++++++++--- core/libs/tests/GuestUnwindModuleInfo.cpp | 10 +++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 114 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 8ed9239477..7d3a322154 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -38,6 +38,52 @@ void* openHostModule(const char* name) { return dlopen_nid_postfix(relative.c_str(), kRtldNow); } +#ifdef _WIN32 +struct ImageName { + DWORD stamp = 0; + DWORD checksum = 0; + DWORD size = 0; + std::string name; +}; + +std::string queryMappedFileName(void* address) { + char path[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), address, path, sizeof(path) - 1); + return std::string(path, length); +} + +void copyMappedFileName(const MEMORY_BASIC_INFORMATION& mbi, ModuleInfoForUnwind* info) { + std::string name; + if (mbi.Type != MEM_IMAGE) { + name = queryMappedFileName(mbi.BaseAddress); + } else { + const auto* base = static_cast(mbi.AllocationBase); + const auto* dos = reinterpret_cast(base); + const auto* nt = dos->e_magic == IMAGE_DOS_SIGNATURE ? reinterpret_cast(base + dos->e_lfanew) : nullptr; + if (nt == nullptr || nt->Signature != IMAGE_NT_SIGNATURE) { + name = queryMappedFileName(mbi.BaseAddress); + } else { + static std::mutex mutex; + static auto& names = *new std::unordered_map(); + std::lock_guard lock(mutex); + auto& cached = names[mbi.AllocationBase]; + if (cached.name.empty() || cached.stamp != nt->FileHeader.TimeDateStamp || cached.checksum != nt->OptionalHeader.CheckSum || + cached.size != nt->OptionalHeader.SizeOfImage) { + cached.stamp = nt->FileHeader.TimeDateStamp; + cached.checksum = nt->OptionalHeader.CheckSum; + cached.size = nt->OptionalHeader.SizeOfImage; + cached.name = queryMappedFileName(mbi.BaseAddress); + } + std::strncpy(info->name, cached.name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; + return; + } + } + std::strncpy(info->name, name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} +#endif + bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { #ifdef _WIN32 MEMORY_BASIC_INFORMATION mbi{}; @@ -50,11 +96,7 @@ bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { info->eh_frame_size = 0; info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; - char path[4096] = {}; - DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); - path[len] = '\0'; - std::strncpy(info->name, path, sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; + copyMappedFileName(mbi, info); return true; #else const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index d196d86131..907b99578e 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" @@ -24,18 +26,59 @@ #ifdef _WIN32 namespace { -void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { +struct UnwindImage { + DWORD stamp = 0; + DWORD checksum = 0; + DWORD size = 0; + bool hasTables = false; + EhFrame::Tables tables; + std::uint32_t headerWord = 0; + std::uint32_t framesWord = 0; + std::string name; +}; + +std::uint32_t ReadWord(std::uint64_t address) { + std::uint32_t value; + std::memcpy(&value, reinterpret_cast(address), sizeof(value)); + return value; +} + +void FillUnwindImage(const std::uint8_t* base, ModuleInfoForUnwind* info) { const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); - EhFrame::Tables tables; - if (!EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", tables)) return; - info->eh_frame_hdr_addr = tables.header; - info->eh_frame_addr = tables.frames; - info->eh_frame_size = tables.framesSize; - info->seg0_addr = reinterpret_cast(base); - info->seg0_size = nt->OptionalHeader.SizeOfImage; + static std::mutex mutex; + static auto& images = *new std::unordered_map(); + std::lock_guard lock(mutex); + auto& image = images[reinterpret_cast(base)]; + const bool current = !image.name.empty() && image.stamp == nt->FileHeader.TimeDateStamp && image.checksum == nt->OptionalHeader.CheckSum && + image.size == nt->OptionalHeader.SizeOfImage && + (!image.hasTables || (ReadWord(image.tables.header) == image.headerWord && ReadWord(image.tables.frames) == image.framesWord)); + if (!current) { + UnwindImage fresh; + fresh.stamp = nt->FileHeader.TimeDateStamp; + fresh.checksum = nt->OptionalHeader.CheckSum; + fresh.size = nt->OptionalHeader.SizeOfImage; + fresh.hasTables = EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); + if (fresh.hasTables) { + fresh.headerWord = ReadWord(fresh.tables.header); + fresh.framesWord = ReadWord(fresh.tables.frames); + } + char path[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), const_cast(base), path, sizeof(path) - 1); + fresh.name.assign(path, length); + image = std::move(fresh); + } + if (image.hasTables) { + info->eh_frame_hdr_addr = image.tables.header; + info->eh_frame_addr = image.tables.frames; + info->eh_frame_size = image.tables.framesSize; + info->seg0_addr = reinterpret_cast(base); + info->seg0_size = image.size; + } + std::strncpy(info->name, image.name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; } } #elif !defined(__APPLE__) @@ -113,9 +156,12 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->eh_frame_size = 0; info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; - if (mbi.Type == MEM_IMAGE) FillGuestUnwindInfo(static_cast(mbi.AllocationBase), info); + if (mbi.Type == MEM_IMAGE) { + FillUnwindImage(static_cast(mbi.AllocationBase), info); + return 0; + } char path[4096] = {}; - DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); + const DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); path[len] = '\0'; std::strncpy(info->name, path, sizeof(info->name) - 1); info->name[sizeof(info->name) - 1] = '\0'; diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index b5af722f49..31d92cd007 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -2,8 +2,10 @@ #include "tests/GuestUnwindModuleInfoFixture.hpp" #include #include +#include #include #include +#include extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); @@ -38,6 +40,14 @@ int main() { const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(base + dos->e_lfanew); Require(info.seg0_size == nt->OptionalHeader.SizeOfImage); + char mapped[4096] = {}; + Require(GetMappedFileNameA(GetCurrentProcess(), memory.BaseAddress, mapped, sizeof(mapped) - 1) != 0); + Require(std::strcmp(info.name, mapped) == 0); + for (int repeat = 0; repeat < 3; ++repeat) { + ModuleInfoForUnwind again{}; + Require(sceKernelGetModuleInfoForUnwind(address, 0, &again) == 0); + Require(std::memcmp(&again, &info, sizeof(info)) == 0); + } fixture->header.version = 2; Require(Throws(address)); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index edd23403ca..59fff7e1d0 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,6 +30,7 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp), [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libkernel, libSceSysmodule) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base, keyed with the image's time stamp, check sum and size. A cached entry is reused only while the `eh_frame_hdr` header word and the first `eh_frame` length word still match, so a change deeper in the tables, or a different image with the same base, time stamp, check sum and size, is not noticed. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it From 12d07b8edd761d5a17674056bc07a45d917e777a Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Tue, 13 Oct 2026 08:58:19 +0200 Subject: [PATCH 047/353] fix(agc): give R8 and R8G8 no sRGB twin where the device decodes them in the shader SrgbTwinFormat mapped R8_UNORM and R8G8_UNORM to R8_SRGB and R8G8_SRGB without asking the device. Where 8_SRGB or 8_8_SRGB is decoded in the shader (context.srgbDecodeFormats: the device cannot sample the sRGB format, or APS5_SRGB_SHADER_DECODE=1), the forced view would have been created with a format the device cannot sample. Those two formats now get no twin there, so a FORCE_DEGAMMA sampler on them keeps the refusal. The image is no longer created MUTABLE_FORMAT either. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/include/Texture.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 14 +++++++++----- .../prx/libSceAgcDriver/tests/TextureFormat.cpp | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 639bff720d..eb0d0c48f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -142,7 +142,7 @@ private: }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); -VkFormat SrgbTwinFormat(VkFormat format); +VkFormat SrgbTwinFormat(const Context& context, VkFormat format); // A guest texture a shader writes through a storage image. It is uploaded like a sampled texture; // after the GPU work completes its results are stored to guest memory (retiled, changed bytes only), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a319e3d8c0..46f60865cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -267,7 +268,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto linearBytes = sliceLinearBytes * arrayLayers; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); + imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); imageInfo.imageType = ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; @@ -698,7 +699,7 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma } void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { - const auto format = SrgbTwinFormat(viewInfo.format); + const auto format = SrgbTwinFormat(context, viewInfo.format); if (format == VK_FORMAT_UNDEFINED) return; viewInfo.pNext = nullptr; viewInfo.format = format; @@ -888,10 +889,13 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear); } -VkFormat SrgbTwinFormat(VkFormat format) { +VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { + // Devices that decode 8_SRGB or 8_8_SRGB in the shader cannot sample R8_SRGB or R8G8_SRGB (or are forced + // not to), so those formats keep no sRGB twin and a FORCE_DEGAMMA sampler on them stays refused. + const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; }; switch (format) { - case VK_FORMAT_R8_UNORM: return VK_FORMAT_R8_SRGB; - case VK_FORMAT_R8G8_UNORM: return VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 26ad1c130b..155eddfcfa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -1,6 +1,7 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include @@ -46,6 +47,20 @@ void srgbDecodeTests() { Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views"); } +void srgbTwinTests() { + constexpr std::uint32_t srgb8 = 1u; + constexpr std::uint32_t srgb8_8 = 2u; + Context context{}; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins"); + context.srgbDecodeFormats = srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader"); + context.srgbDecodeFormats = srgb8 | srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); + Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); +} + template void reject(TAction action, std::string_view reason) { try { @@ -100,6 +115,7 @@ void convertedDccClearTests() { void RunTextureFormatTests() { srgbDecodeTests(); + srgbTwinTests(); packedChannelTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM"); From 007721d43e0511e446cd59d2854fe6ff80ff9a69 Mon Sep 17 00:00:00 2001 From: TideOPL Date: Mon, 12 Oct 2026 22:23:48 +0100 Subject: [PATCH 048/353] perf(shader): index specialization tables by SPIR-V id SpecializeSpirv kept its per-id data (folded values, result types, scalar types, removed ids, blocks, edges, copies, definitions and the live sets) in std::map and std::set, and repeats fold, prune and copy propagation over the whole module until nothing changes. Ids are dense below the header's bound, so the tables become vectors indexed by id. Lookups of operands that are literals rather than ids stay bounds-checked, iteration keeps ascending id order, and an id at or beyond the bound now throws. --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 14 ++ .../SpirvBackend/src/SpirvSpecialization.cpp | 172 ++++++++++++++---- 2 files changed, 152 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index cf6e8a94fe..d067e97200 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -2,6 +2,7 @@ #include "SpirvBackend/SpirvSpecialization.hpp" #include #include +#include #include #include #include @@ -285,8 +286,21 @@ void testFoldingLeavesNoDeadBlocksOrConstants() { checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids"); } +void testIdsBeyondTheBoundAreRejected() { + auto words = foldedSwitchModule(13u, {1u, 1u, 1u}); + words[3] = 12u; + bool rejected = false; + try { + static_cast(ShaderRecompiler::SpecializeSpirv(words)); + } catch (const std::exception&) { + rejected = true; + } + check(rejected, "a module whose ids reach its bound was specialized"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); + testIdsBeyondTheBoundAreRejected(); testStructExtractsAreNotShuffled(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index d9cc99512a..441a136b15 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -428,11 +428,105 @@ struct ScalarType { bool boolean; }; +template +class IdTable { +public: + void Reset(std::size_t bound) { + items.assign(bound, T{}); + present.assign(bound, 0u); + count = 0; + } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + const T* find(std::uint32_t id) const { return contains(id) ? &items[id] : nullptr; } + const T& at(std::uint32_t id) const { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + T& at(std::uint32_t id) { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + bool emplace(std::uint32_t id, T value) { + check(id); + if (present[id] != 0u) return false; + items[id] = std::move(value); + present[id] = 1u; + ++count; + return true; + } + T& operator[](std::uint32_t id) { + check(id); + if (present[id] == 0u) { + items[id] = T{}; + present[id] = 1u; + ++count; + } + return items[id]; + } + void erase(std::uint32_t id) { + if (!contains(id)) return; + present[id] = 0u; + items[id] = T{}; + --count; + } + std::size_t size() const { return count; } + bool empty() const { return count == 0; } + class Iterator { + public: + Iterator(IdTable& table, std::uint32_t id) : table(table), id(id) { skip(); } + std::pair operator*() const { return {id, table.items[id]}; } + Iterator& operator++() { + ++id; + skip(); + return *this; + } + bool operator!=(const Iterator& other) const { return id != other.id; } + + private: + void skip() { + while (id < table.present.size() && table.present[id] == 0u) ++id; + } + IdTable& table; + std::uint32_t id; + }; + Iterator begin() { return Iterator(*this, 0u); } + Iterator end() { return Iterator(*this, static_cast(present.size())); } + +private: + void check(std::uint32_t id) const { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + } + std::vector items; + std::vector present; + std::size_t count = 0; +}; + +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + class Specialization { public: explicit Specialization(std::span words) { if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared SPIR-V header"); header.assign(words.begin(), words.begin() + 5); + bound = header[3]; + types.Reset(bound); + values.Reset(bound); + resultTypes.Reset(bound); + vectorTypes.Reset(bound); + removed.Reset(bound); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared SPIR-V instruction"); @@ -479,15 +573,15 @@ public: private: std::optional value(std::uint32_t id) const { - const auto found = values.find(id); - return found != values.end() ? std::optional(found->second) : std::nullopt; + const auto* found = values.find(id); + return found != nullptr ? std::optional(*found) : std::nullopt; } std::optional evaluate(const Instruction& instruction) const { const auto op = Opcode(instruction); if (instruction.size() < 4u) return std::nullopt; - const auto type = types.find(instruction[1]); - if (type == types.end() || type->second.width > 32u) return std::nullopt; + const auto* type = types.find(instruction[1]); + if (type == nullptr || type->width > 32u) return std::nullopt; const auto left = value(instruction[3]); if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; @@ -505,8 +599,8 @@ private: case spv::OpBitwiseAnd: return *left & *right; case spv::OpBitwiseOr: return *left | *right; case spv::OpBitwiseXor: return *left ^ *right; - case spv::OpShiftRightLogical: return *right < type->second.width ? std::optional(*left >> *right) : std::nullopt; - case spv::OpShiftLeftLogical: return *right < type->second.width ? std::optional(*left << *right) : std::nullopt; + case spv::OpShiftRightLogical: return *right < type->width ? std::optional(*left >> *right) : std::nullopt; + case spv::OpShiftLeftLogical: return *right < type->width ? std::optional(*left << *right) : std::nullopt; case spv::OpIEqual: return *left == *right; case spv::OpINotEqual: return *left != *right; case spv::OpULessThan: return *left < *right; @@ -532,7 +626,8 @@ private: bool fold() { bool changed = false; bool function = false; - std::map> extracts; + IdTable> extracts; + extracts.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); @@ -554,14 +649,14 @@ private: op = Opcode(instruction); if (op == spv::OpCompositeExtract && instruction.size() == 5u) extracts.emplace(instruction[2], std::pair{instruction[3], instruction[4]}); if (op == spv::OpCompositeConstruct && instruction.size() == 7u) { - const auto first = extracts.find(instruction[3]); - bool shuffle = first != extracts.end() && vectorTypes.contains(instruction[1]) && resultTypes.contains(first->second.first) && vectorTypes.contains(resultTypes.at(first->second.first)); + const auto* first = extracts.find(instruction[3]); + bool shuffle = first != nullptr && vectorTypes.contains(instruction[1]) && resultTypes.contains(first->first) && vectorTypes.contains(resultTypes.at(first->first)); for (std::size_t index = 4; shuffle && index < instruction.size(); ++index) { - const auto found = extracts.find(instruction[index]); - shuffle = found != extracts.end() && found->second.first == first->second.first; + const auto* found = extracts.find(instruction[index]); + shuffle = found != nullptr && found->first == first->first; } if (shuffle) { - const auto source = first->second.first; + const auto source = first->first; bool identity = resultTypes.contains(source) && resultTypes.at(source) == instruction[1]; Instruction replacement = Make(spv::OpVectorShuffle, {instruction[1], instruction[2], source, source}); for (std::size_t index = 3; index < instruction.size(); ++index) { @@ -587,7 +682,8 @@ private: } bool prune() { - std::map> blocks; + IdTable> blocks; + blocks.Reset(bound); std::vector entries; std::uint32_t block = 0; bool entry = false; @@ -622,7 +718,8 @@ private: } return true; }; - std::set loopHeaders; + IdSet loopHeaders; + loopHeaders.Reset(bound); std::map continueHeaders; for (const auto& [label, indices] : blocks) { for (const auto index : indices) { @@ -631,7 +728,8 @@ private: continueHeaders.emplace(instructions[index].at(2), label); } } - std::map> edges; + IdTable> edges; + edges.Reset(bound); for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; auto op = Opcode(terminal); @@ -668,18 +766,19 @@ private: else if (op == spv::OpBranchConditional) successors = {terminal.at(2), terminal.at(3)}; else if (op == spv::OpSwitch) { successors.push_back(terminal.at(2)); - const auto selectorType = resultTypes.find(terminal.at(1)); - const auto stride = selectorType != resultTypes.end() && types.contains(selectorType->second) && types.at(selectorType->second).width == 64u ? 3u : 2u; + const auto* selectorType = resultTypes.find(terminal.at(1)); + const auto stride = selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u ? 3u : 2u; for (std::size_t index = 3; index + stride <= terminal.size(); index += stride) successors.push_back(terminal[index + stride - 1u]); } } - std::set live; + IdSet live; + live.Reset(bound); auto pending = entries; while (!pending.empty()) { const auto label = pending.back(); pending.pop_back(); if (!blocks.contains(label)) throw std::runtime_error("prepared branch references a missing block"); - if (!live.insert(label).second) continue; + if (!live.insert(label)) continue; const auto& successors = edges.at(label); pending.insert(pending.end(), successors.begin(), successors.end()); } @@ -730,14 +829,15 @@ private: return changed; } - std::set referencedIds() const { - std::set referenced; + IdSet referencedIds() const { + IdSet referenced; + referenced.Reset(bound); for (const auto& instruction : instructions) { if (instruction.empty()) continue; std::uint32_t switchLiteralWords = 1u; if (Opcode(instruction) == spv::OpSwitch && instruction.size() > 1u) { - const auto selectorType = resultTypes.find(instruction[1]); - if (selectorType != resultTypes.end() && types.contains(selectorType->second) && types.at(selectorType->second).width == 64u) switchLiteralWords = 2u; + const auto* selectorType = resultTypes.find(instruction[1]); + if (selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u) switchLiteralWords = 2u; } VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { referenced.insert(instruction[index]); }); } @@ -800,7 +900,8 @@ private: } bool propagateCopies() { - std::map copies; + IdTable copies; + copies.Reset(bound); for (const auto& instruction : instructions) { if (Opcode(instruction) != spv::OpCopyObject) continue; if (instruction.size() != 4u) throw std::runtime_error("invalid prepared copy instruction"); @@ -845,7 +946,8 @@ private: } void removeDeadComputations() { - std::map definitions; + IdTable definitions; + definitions.Reset(bound); std::vector pending; bool function = false; for (std::size_t index = 0; index < instructions.size(); ++index) { @@ -871,13 +973,14 @@ private: const auto resultIndex = hasResult ? (hasType ? 2u : 1u) : 0u; for (std::size_t operand = 1; operand < instruction.size(); ++operand) if (operand != resultIndex) pending.push_back(instruction[operand]); } - std::set live; + IdSet live; + live.Reset(bound); while (!pending.empty()) { const auto id = pending.back(); pending.pop_back(); - const auto found = definitions.find(id); - if (found == definitions.end() || !live.insert(id).second) continue; - const auto& instruction = instructions[found->second]; + const auto* found = definitions.find(id); + if (found == nullptr || !live.insert(id)) continue; + const auto& instruction = instructions[*found]; if (!VisitInputs(instruction, [&](std::size_t operand) { pending.push_back(instruction[operand]); })) throw std::runtime_error("missing prepared pure instruction operands"); } for (const auto& [id, index] : definitions) { @@ -890,11 +993,12 @@ private: std::vector header; std::vector instructions; std::vector constants; - std::map types; - std::map values; - std::map resultTypes; - std::set vectorTypes; - std::set removed; + std::uint32_t bound = 0; + IdTable types; + IdTable values; + IdTable resultTypes; + IdSet vectorTypes; + IdSet removed; }; } From a141b8eb67e7b01c1c5752988888947907104e33 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Tue, 13 Oct 2026 09:09:33 +0200 Subject: [PATCH 049/353] fix(agc): drop the comment in SrgbTwinFormat Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 46f60865cd..03f45dbc53 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -890,8 +890,6 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc } VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { - // Devices that decode 8_SRGB or 8_8_SRGB in the shader cannot sample R8_SRGB or R8G8_SRGB (or are forced - // not to), so those formats keep no sRGB twin and a FORCE_DEGAMMA sampler on them stays refused. const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; }; switch (format) { case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; From 9cdcd581715644c7ad7ec86d996e4eed50e97127 Mon Sep 17 00:00:00 2001 From: Doston Abdullayev Date: Tue, 13 Oct 2026 07:11:08 +0000 Subject: [PATCH 050/353] fix(libkernel): use precise waits for empty poll sets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry the poll-only portion of Doston Abdullayev’s public #1471 fix, preserving its original authorship. The select timeout change is already covered separately by #4754. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 3 ++- core/libs/tests/GuestPoll.cpp | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index a012fe1960..ef6134b68e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -16,6 +16,7 @@ #include #endif #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" @@ -1224,7 +1225,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (native.empty() && pipes.empty()) { if (ready || timeout == 0) return ready; if (timeout < 0) throw std::runtime_error("poll: an infinite wait without sockets or pipes never returns"); - std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + PreciseSleepUs(static_cast(timeout) * 1000); return 0; } const auto collect = [&](std::size_t i, bool pipe) { diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 446ff33ddf..3cef16d2ba 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -142,6 +142,10 @@ int main() { PollDescriptor closed[2]{{client, In, 0}, {listener, In, 0}}; Require(poll_nid_postfix(closed, 2, -1) == 1 && closed[0].revents == Invalid && closed[1].revents == 0); Require(poll_nid_postfix(nullptr, 0, 10) == 0); + const auto shortWaitStart = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(nullptr, 0, 1) == 0); + const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200)); Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); TestPipes(listener); From 6706e44d95c50ea071208939e09dc20970d0592d Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:01:47 +0200 Subject: [PATCH 051/353] fix(conventions): ignore C++ raw literal contents --- tools/check_conventions.py | 28 +++++-- .../tests/test_check_conventions_comments.py | 83 +++++++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index de46dd4d96..9e24c59d31 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -6,9 +6,12 @@ import re import subprocess import sys from collections import defaultdict -from pathlib import PurePosixPath +from pathlib import Path, PurePosixPath from urllib.parse import unquote +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from nid_names import CPP_TOKEN + RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md or docs/dev/debt/ in the same pull request; #endif and namespace ends are always allowed"), "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed under Silent stubs in docs/dev/debt/.md, by their file or by name with a path in their library"), @@ -36,7 +39,6 @@ DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') -TOKENS = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'|//[^\n]*|/\*.*?\*/', re.S) ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" @@ -76,7 +78,7 @@ class Check: self.numstat[path] = (added, deleted) paths = iter(self.status) path, number = None, None - for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).splitlines(): + for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).split("\n"): if line.startswith("diff --git "): path, number = next(paths), None elif line.startswith("@@"): @@ -99,9 +101,10 @@ class Check: exports = {} for path, lines in self.files(lambda p: p.suffix in CPP): core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts - source = None + source = self.show(path).split("\n") + masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") for number, text in lines: - bare = STRINGS.sub('""', text) + bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): self.report("comment", path, number) if EXTENSION.search(bare): @@ -130,7 +133,7 @@ class Check: return pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" result = subprocess.run(["git", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") - for line in result.stdout.splitlines(): + for line in filter(None, result.stdout.split("\n")): _, path, number, text = line.split(":", 3) name = EXPORT.search(text).group(1) own, own_number = exports[name] @@ -331,9 +334,18 @@ def allowed_notes(path): return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) +def cpp_text(text, preserve_comments=False): + def mask(match): + if match.group("code") is not None or preserve_comments and match.group("comment") is not None: + return match.group() + blank = re.sub(r"[^\n]", " ", match.group()) + return blank if match.group("comment") is not None else '""' + blank[2:] + + return CPP_TOKEN.sub(mask, text) + + def function_body(lines, start): - text = "\n".join(lines[start:start + 400]) - text = TOKENS.sub(lambda match: "" if match.group(0).startswith("/") else '""', text) + text = cpp_text("\n".join(lines[start:start + 400])) brace, semicolon = text.find("{"), text.find(";") if brace < 0 or 0 <= semicolon < brace: return None diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 4d3e83c0bc..dde7ca9628 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -6,6 +6,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output @@ -39,6 +40,18 @@ class FunctionBodyTests(unittest.TestCase): source = 'int APS5_VABI sceExample() {\nuse("// }", "/* { */");\nreturn 0;\n}' self.assertEqual(function_body(source.splitlines(), 0), 'use("","");return0;') + def test_raw_literal_braces_and_comments_preserve_following_statements(self): + source = 'int APS5_VABI sceExample() {\nuse(R"json({"brace":"}","comment":"/* hidden */"})json");\nvalidate();\nreturn 0;\n}' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'use("");validate();return0;') + self.assertIsNone(STUB_BODY.fullmatch(body)) + + def test_raw_literal_return_keeps_a_literal_sentinel(self): + source = 'const char* APS5_VABI sceExample() { return R"json({"url":"https://example.test"})json"; }' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'return"";') + self.assertIsNone(STUB_BODY.fullmatch(body)) + def test_apostrophes_in_comments_do_not_open_character_literals(self): source = "int APS5_VABI sceExample() {\n// don't fail here\nvalidate();\n// it's fine\nreturn 0;\n}" body = function_body(source.splitlines(), 0) @@ -58,6 +71,59 @@ class FunctionBodyTests(unittest.TestCase): self.assertIsNone(function_body(source.splitlines(), 0)) +class RawLiteralCommentTests(unittest.TestCase): + path = "core/example.cpp" + + def findings(self, source, added): + check = Check.__new__(Check) + check.tree = set() + check.status = {} + check.findings = [] + lines = source.split("\n") + check.added = {self.path: [(number, lines[number - 1]) for number in added]} + with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): + check.code() + return check.findings + + def test_isolated_checker_loads_its_trusted_sibling_from_another_directory(self): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + Path(directory, "nid_names.py").write_text('raise RuntimeError("untrusted current directory")\n', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(checker), "--help"], cwd=directory, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("check a branch", result.stdout) + + def test_raw_url_does_not_create_a_comment(self): + source = 'static const char* Metadata = R"json({"url":"https://example.test"})json";' + self.assertEqual(self.findings(source, [1]), []) + + def test_blank_added_cpp_lines_preserve_positions(self): + for source, added in (("\n", [1]), ("int value;\n\n", [2])): + with self.subTest(source=source): + self.assertEqual(self.findings(source, added), []) + + def test_unicode_separators_in_literals_keep_physical_lines(self): + for separator in ("\x85", "\u2028", "\u2029"): + with self.subTest(separator=separator): + source = f'static const char* Metadata = R"text(https://example.test/{separator}more)text";\nint value; // pending\n' + self.assertEqual(self.findings(source, [1, 2]), [("comment", self.path, 2, "")]) + + def test_added_line_inside_an_existing_multiline_raw_literal_is_not_a_comment(self): + source = 'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text";\n' + self.assertEqual(self.findings(source, [2]), []) + + def test_actual_comment_after_a_raw_literal_retains_its_line(self): + for comment in ("// pending implementation", "/* pending implementation */"): + with self.subTest(comment=comment): + source = f'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text"; {comment}\n' + self.assertEqual(self.findings(source, [2, 3]), [("comment", self.path, 3, "")]) + + def test_quotes_and_raw_looking_text_do_not_hide_actual_comments(self): + for comment in ('// " R"text(https://example.test)text"', '/* " R"text(https://example.test)text" */'): + with self.subTest(comment=comment): + self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) + + class SilentStubTests(unittest.TestCase): def test_commented_stub_is_reported_when_debt_change_allows_comments(self): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" @@ -370,6 +436,23 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + def test_unicode_raw_literals_keep_git_patch_and_grep_records_intact(self): + literals = 'R"text(foo\u2028+tail\u2029+tail)text"' + fixtures = ( + ("core/example.cpp", f"static const char* Metadata = {literals};\n"), + ("core/libs/prx/libSceExample/Export.cpp", f"const char* APS5_VABI sceGetLabel() {{ return {literals}; }}\n"), + ) + for name, definition in fixtures: + with self.subTest(path=name): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(definition + "int value; // pending\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + self.assertEqual([number for number, _ in check.added[name]], [1, 2]) + check.code() + self.assertEqual(check.findings, [("comment", name, 2, "")]) + def test_checks_from_a_subdirectory_see_the_whole_repository(self): definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" first = self.root / "core/libs/prx/libSceA/Export.cpp" From 0daf1d5ba3d5277d109ebc320548f04fae7f5e13 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:55:31 -0400 Subject: [PATCH 052/353] fix(libSceNet): report SO_ERROR from sceNetGetsockopt --- core/libs/prx/libSceNet/Export.cpp | 28 +++++++++++++++++++++++---- core/libs/tests/GuestSceNet.cpp | 31 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 93f56d7b57..d1825c3d09 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -82,6 +82,7 @@ constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; +constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; @@ -104,8 +105,8 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int native_error() { - switch (WSAGetLastError()) { +int net_error(int code) { + switch (code) { case WSAEWOULDBLOCK: return NET_EAGAIN; case WSAEINPROGRESS: return NET_EINPROGRESS; case WSAEALREADY: return NET_EALREADY; @@ -130,6 +131,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; using NativeLength = socklen_t; @@ -137,8 +139,8 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int native_error() { - switch (errno) { +int net_error(int code) { + switch (code) { case EAGAIN: return NET_EAGAIN; #if EWOULDBLOCK != EAGAIN case EWOULDBLOCK: return NET_EAGAIN; @@ -166,6 +168,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(errno); } #endif struct NativeSocketHandle { @@ -241,6 +244,7 @@ struct Sock { bool aborted = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; + int reported_error = 0; }; #ifdef _WIN32 @@ -756,6 +760,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it == g_socks.end()) return fail(NET_EBADF); + it->second.reported_error = 0; socket = it->second; } sockaddr_storage native{}; @@ -992,6 +997,21 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_ERROR: { + int pending = 0; + NativeLength size = sizeof(pending); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size) != 0) + return fail(native_error()); +#ifdef _WIN32 + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + if (pending != 0 && pending == it->second.reported_error) pending = 0; + else if (pending != 0) it->second.reported_error = pending; +#endif + value = pending == 0 ? 0 : net_error(pending); + break; + } default: return fail(NET_EOPNOTSUPP); } std::memcpy(optval, &value, sizeof(value)); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 96d7ded248..fb8e890bb5 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -463,6 +463,36 @@ static void CheckIpv6SocketsAreIpv6Only() { Require(sceNetSocketClose(inet) == 0); } +static void CheckSocketError() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket(nullptr, 2, 1, 6); + Require(listener >= 0 && sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0 && size == address.size()); + const int connected = sceNetSocket(nullptr, 2, 1, 6); + Require(connected >= 0 && sceNetConnect(connected, address.data(), address.size()) == 0); + int error = -1; + size = sizeof(error); + Require(sceNetGetsockopt(connected, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error) && error == 0); + for (const int descriptor : {connected, listener}) Require(sceNetSocketClose(descriptor) == 0); + + const int refused = sceNetSocket(nullptr, 2, 1, 6); + const int nonblocking = 1; + Require(refused >= 0 && sceNetSetsockopt(refused, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + const int started = sceNetConnect(refused, address.data(), address.size()); + Require(started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36); + error = 0; + for (int attempt = 0; attempt < 500 && error == 0; ++attempt) { + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error)); + if (error == 0) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(error == 61); + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && error == 0); + Require(sceNetSocketClose(refused) == 0); +} + int main() { const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); @@ -495,6 +525,7 @@ int main() { Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); + CheckSocketError(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323dc..5384786b44 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -9,5 +9,6 @@ ### Functional - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From 7ebad36341f93fb458e2a1521c8037925240300f Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:23:28 +0200 Subject: [PATCH 053/353] fix(tools): accept skips on displays without 120 Hz --- tools/check_skips.py | 10 ++++++---- tools/tests/test_check_skips.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index e669234ee6..d800af6dfd 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -7,6 +7,8 @@ DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports| NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) +HIGH_REFRESH_LIMITATION = re.compile(r"^skipped, current display does not provide 120 Hz[ \t]*$", re.IGNORECASE | re.MULTILINE) + WINDOWS_PREFIX = "agc" @@ -41,11 +43,11 @@ if __name__ == "__main__": print(f"{name}: {reason}") raise SystemExit if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output)] - expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device" + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not HIGH_REFRESH_LIMITATION.search(output)] + expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: - unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output)] - expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension or no display for a Vulkan window" + unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not HIGH_REFRESH_LIMITATION.search(output)] + expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") if unexpected: diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index ce1b342961..710df2240c 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -34,6 +34,41 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_missing_high_refresh_display_is_expected_in_both_jobs(self): + reason = "current display does not provide 120 Hz" + reports = ( + f'skipped, {reason}', + f'', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 test(s) skipped, all expected", result.stdout) + + def test_unknown_display_reasons_stay_unexpected_in_both_jobs(self): + reasons = ( + "current display is broken", + "current display does not provide 120 Hz because the test is flaky", + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for reason in reasons: + junit.write_text(f'skipped, {reason}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, reason=reason): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + def test_disabled_tests_fail_in_both_jobs_including_agc(self): xml = '' with tempfile.TemporaryDirectory() as directory: From 74e239408b61b1a64f80314908f14cbfef865e96 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:25:12 +0200 Subject: [PATCH 054/353] fix(tools): reject mixed high refresh skip diagnostics --- tools/check_skips.py | 17 +++++++++++++--- tools/tests/test_check_skips.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index d800af6dfd..0cc51cacb3 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -7,11 +7,22 @@ DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports| NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) -HIGH_REFRESH_LIMITATION = re.compile(r"^skipped, current display does not provide 120 Hz[ \t]*$", re.IGNORECASE | re.MULTILINE) +HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) WINDOWS_PREFIX = "agc" +def high_refresh_limitation(output): + reasons = [] + for line in output.splitlines(): + reason = line.strip() + if reason.lower().startswith("skipped,"): + reason = reason[len("skipped,"):].strip() + if reason: + reasons.append(reason) + return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) + + def skips(path): for case in ET.parse(path).getroot().iter("testcase"): if case.get("status") == "disabled": @@ -43,10 +54,10 @@ if __name__ == "__main__": print(f"{name}: {reason}") raise SystemExit if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not HIGH_REFRESH_LIMITATION.search(output)] + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: - unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not HIGH_REFRESH_LIMITATION.search(output)] + unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 710df2240c..c2a4c33b79 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -39,6 +39,12 @@ class CheckSkipsTests(unittest.TestCase): reports = ( f'skipped, {reason}', f'', + f'{reason}', + f'skipped, {reason}', + f'\n\n{reason}\n' + f'\n\nskipped, {reason}\n\n' + f'\nskipped, {reason}\n', + f'\n\n{reason}\n\n{reason}\n', ) with tempfile.TemporaryDirectory() as directory: junit = Path(directory) / "junit.xml" @@ -52,6 +58,35 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("1 test(s) skipped, all expected", result.stdout) + def test_high_refresh_reason_cannot_hide_additional_diagnostics(self): + reason = "current display does not provide 120 Hz" + known = f"skipped, {reason}" + extra = "unexpected failure detail" + reports = ( + f'{known}\n{extra}', + f'{extra}\n{known}', + f'{known}\n{extra}', + f'{known}{extra}', + f'{extra}{known}', + f'{extra}', + f'{reason}{extra}', + f'{extra}', + f'{known}', + f'{reason}\n{extra}', + f'{known}\n{known}\n{extra}', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + def test_unknown_display_reasons_stay_unexpected_in_both_jobs(self): reasons = ( "current display is broken", From 6fb0727c48f878da6a98df243df1f530d236c167 Mon Sep 17 00:00:00 2001 From: ideabsassistenza Date: Tue, 13 Oct 2026 09:31:28 +0200 Subject: [PATCH 055/353] fix(debt): preserve leading entries during conflict resolution --- tools/debt_split.py | 2 +- tools/tests/test_debt_split.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index bfb7f9d522..8b5ad131eb 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -163,7 +163,7 @@ def apply_change(root, base, new, index_text): _, section, item, name = entry entries = load(name).setdefault(section, []) moved = relink(item, INDEX, debt_file(name)) - position = len(entries) + position = 0 if anchor is None else len(entries) if anchor is not None and anchor[1] == section and anchor[3] == name: anchored = relink(anchor[2], INDEX, debt_file(name)) if anchored in entries: diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 1b5c9acc3e..840a0097fb 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -136,6 +136,18 @@ class DebtSplitTests(unittest.TestCase): self.assertNotEqual(result.returncode, 0) self.assertIn("not in a three-way conflict", result.stderr) + def test_resolve_preserves_entries_inserted_before_the_first_existing_entry(self): + first = "- [libSceA](../../core/libs/prx/libSceA/First.cpp) - `sceAFirst` returns 0" + second = "- [libSceA](../../core/libs/prx/libSceA/Second.cpp) - `sceASecond` returns 0" + result = self.rebase([first, second, A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in (first, second, A_STUB)) + + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + if __name__ == "__main__": unittest.main() From e8fd542f0314cd4d0ee3f18b30eb2bd4232ef108 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 16:17:21 +0200 Subject: [PATCH 056/353] feat(libkernel): implement pthread_atfork pthread_atfork (U9t5kJAWPnA, aerolib) appends the prepare, parent and child handlers to a process-wide list under a lock and returns 0, or ENOMEM when the entry cannot be allocated, as FreeBSD 11.4's _pthread_atfork does (lib/libthr/thread/thr_fork.c:78-100, freebsd-src 8c639945a44588f6cfdddfb057bb5aad88735da0). Null handlers are accepted; FreeBSD's fork skips them. The handlers never run: fork (libkernel_sys) throws and vfork is absent, so no child process is created. The list is kept, in registration order, for a later fork (PthreadAtforkHandlers_nid_no_patch); recorded in TechnicalDebt. XPSemu (PPSA97358) calls it from a constructor, before main (ZiZc3/XPSemu 8eec07b4, util/rcu.c:488-495): static void __attribute__((__constructor__)) rcu_init(void) { smp_mb_global_init(); #ifdef CONFIG_POSIX pthread_atfork(rcu_init_lock, rcu_init_unlock, rcu_init_child); #endif rcu_init_complete(); } so a throwing export would stop the title before main. Predicted stop removed (registry index 255, not run): FAIL: unresolved ELF import U9t5kJAWPnA from libkernel.prx guest_pthread_atfork checks registration order, null handlers, concurrent registration from four threads and that no handler runs. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit f37bccf6efd76edcde5d582800c89b83fdd30472) --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ .../prx/libkernel/Pthread/Posix/Atfork.cpp | 24 ++++++++++ .../prx/libkernel/Pthread/include/Atfork.hpp | 19 ++++++++ core/libs/tests/GuestPthreadAtfork.cpp | 48 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 102 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp create mode 100644 core/libs/prx/libkernel/Pthread/include/Atfork.hpp create mode 100644 core/libs/tests/GuestPthreadAtfork.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 449707ab64..d03c5fd4dc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Atfork.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Cond.cpp @@ -640,6 +641,15 @@ if(BUILD_TESTING) set_tests_properties(guest_parent_process PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_atfork_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAtfork.cpp) + target_include_directories(guest_pthread_atfork_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_atfork_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_atfork_tests) + add_test(NAME guest_pthread_atfork COMMAND guest_pthread_atfork_tests) + if(WIN32) + set_tests_properties(guest_pthread_atfork PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kill_tests ${LIBS_INCLUDE_DIR}/tests/GuestKill.cpp) target_include_directories(guest_kill_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kill_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp new file mode 100644 index 0000000000..2becc20064 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp @@ -0,0 +1,24 @@ +#include "../include/Atfork.hpp" +#include +#include + +namespace { +constexpr int GuestNoMemory = 12; +std::mutex atforkLock; +std::vector atforkHandlers; +} + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + std::lock_guard lock(atforkLock); + try { + atforkHandlers.push_back({prepare, parent, child}); + } catch (const std::bad_alloc&) { + return GuestNoMemory; + } + return 0; +} + +std::vector PthreadAtforkHandlers_nid_no_patch() { + std::lock_guard lock(atforkLock); + return atforkHandlers; +} diff --git a/core/libs/prx/libkernel/Pthread/include/Atfork.hpp b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp new file mode 100644 index 0000000000..fa50990a6e --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +using PthreadAtforkHandler = void (APS5_VABI*)(); + +struct PthreadAtforkHandlers { + PthreadAtforkHandler prepare; + PthreadAtforkHandler parent; + PthreadAtforkHandler child; +}; + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child); + +std::vector PthreadAtforkHandlers_nid_no_patch(); + +#endif diff --git a/core/libs/tests/GuestPthreadAtfork.cpp b/core/libs/tests/GuestPthreadAtfork.cpp new file mode 100644 index 0000000000..0cf6b91fec --- /dev/null +++ b/core/libs/tests/GuestPthreadAtfork.cpp @@ -0,0 +1,48 @@ +#include "prx/libkernel/Pthread/include/Atfork.hpp" +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}; + +static void APS5_VABI Prepare() { ++calls; } +static void APS5_VABI Parent() { ++calls; } +static void APS5_VABI Child() { ++calls; } + +static bool Same(const PthreadAtforkHandlers& entry, PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + return entry.prepare == prepare && entry.parent == parent && entry.child == child; +} + +int main() { + Require(PthreadAtforkHandlers_nid_no_patch().empty()); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + Require(pthread_atfork_nid_postfix(nullptr, Parent, nullptr) == 0); + Require(pthread_atfork_nid_postfix(nullptr, nullptr, nullptr) == 0); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + auto handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4); + Require(Same(handlers[0], Prepare, Parent, Child)); + Require(Same(handlers[1], nullptr, Parent, nullptr)); + Require(Same(handlers[2], nullptr, nullptr, nullptr)); + Require(Same(handlers[3], Prepare, Parent, Child)); + + constexpr std::size_t threadCount = 4; + constexpr std::size_t perThread = 64; + std::vector threads; + for (std::size_t index = 0; index < threadCount; ++index) { + threads.emplace_back([] { + for (std::size_t count = 0; count < perThread; ++count) Require(pthread_atfork_nid_postfix(nullptr, nullptr, Child) == 0); + }); + } + for (auto& thread : threads) thread.join(); + handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4 + threadCount * perThread); + Require(Same(handlers[3], Prepare, Parent, Child)); + for (std::size_t index = 4; index < handlers.size(); ++index) Require(Same(handlers[index], nullptr, nullptr, Child)); + Require(calls == 0); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ed0375958..d23b1928a5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,4 +158,5 @@ - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. +- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 calls it from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), before `main` - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up From 8d455b80186274c921bfa2cc41df23bfe24e477f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 07:39:26 +0000 Subject: [PATCH 057/353] docs(libkernel): qualify atfork title reachability --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d23b1928a5..901878a60e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,5 +158,5 @@ - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. -- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 calls it from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), before `main` +- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up From 3e238ff11d03beedfc9b224f6c749fd2a4f393ab Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 07:36:44 +0000 Subject: [PATCH 058/353] fix(shader): accept MIMG sampler tuples ending in VCC --- core/libs/tests/ImageSampleGather.cpp | 26 +++++++++++++++++++ .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 95d3352628..6fa633e3a7 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -28,6 +28,30 @@ static void CheckPck(std::uint32_t encoding, RdnaOpcode opcode, std::uint32_t dm Require(instruction.dataDwordCount == dataComponents); Require(IsImageOpcode(instruction.op)); } +static void CheckSamplerVccTuple() { + const std::array code{0xf0800f08u, 26u << 21u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.source1.reg == 0u); + Require(instruction.source2.kind == RdnaOperandKind::ScalarRegister && instruction.source2.reg == 104u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().sampler == 26u); +} +static void CheckSamplerVccTupleOverflow() { + const std::array code{0xf0800f08u, 27u << 21u}; + bool rejected = false; + try { + DecodeRdnaMimg(0u, code, 0u); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} static void CheckPckRejectsZeroMask(std::uint32_t encoding) { const std::array code{(0x3cu << 26u) | (encoding << 18u) | (1u << 3u), 0u}; bool rejected = false; @@ -49,6 +73,8 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string Require(false); } int main() { + CheckSamplerVccTuple(); + CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); Check(0x35u, RdnaOpcode::ImageSampleBO, 0xfu); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index 0ab4c4645b..f64c3c6bd7 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -417,7 +417,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 16u) & 31u) * 4u; const auto sampler = ((word1 >> 21u) & 31u) * 4u; const bool r128 = (word0 & 0x8000u) != 0u; - if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler + 4u > 106u)) { + if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler > 104u)) { throw std::runtime_error("MIMG descriptor register range overflow"); } RdnaInstruction instruction{}; From eefe4db0186940474ee3a5cb37f3d176f7f400c7 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 22:56:17 +0000 Subject: [PATCH 059/353] fix(libc): return native fopen errors --- core/libs/prx/libc/src/FileAndHeap.cpp | 22 ++-------------------- core/libs/tests/FileStreams.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index ef219f06bb..1c14681ecc 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -274,29 +274,11 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) } std::filesystem::path fpath; if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } - const auto abs_path = fpath.string(); std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { const int error = errno; - if (error == ENOENT || (parsed->exclusive && error == EEXIST)) { - errno = error; - return nullptr; - } - const auto reason = std::strerror(error); - std::error_code ec; - std::filesystem::path sibling; - for (const auto& entry : std::filesystem::directory_iterator(fpath.parent_path(), ec)) { - if (ec) break; - if (entry.path().stem() == fpath.stem() && entry.path().extension() != fpath.extension()) { - sibling = std::filesystem::absolute(entry.path()); - break; - } - } - if (!sibling.empty()) { - // APS5_LOG_OUT("%s: \"%s\": %s. Sibling: \"%s\"", FOPEN_MSG_NOT_FOUND, abs_path.c_str(), reason, sibling.filename().string().c_str()); - return nullptr; - } - throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_OPEN_FAILED + ": \"" + abs_path + "\": " + reason); + errno = error; + return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); if (parsed->append) static_cast(std::fseek(handle.get(), 0, SEEK_END)); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index d0431836ba..60a760f1ca 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -75,6 +75,19 @@ int main(int argc, char** argv) { errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); Require(errno == ENOENT); +#ifdef _WIN32 + const std::string directoryMode = "rb"; + constexpr int directoryError = EACCES; +#else + const std::string directoryMode = "wb"; + constexpr int directoryError = EISDIR; +#endif + const std::string directoryName = std::string(argv[1]) + ".dir"; + Require(std::filesystem::create_directory(directoryName)); + errno = 0; + Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr); + Require(errno == directoryError); + Require(std::filesystem::remove(directoryName)); const std::u8string unicodeName = u8"セーブé.tmp"; const std::string guestName(unicodeName.begin(), unicodeName.end()); const std::filesystem::path hostName(unicodeName); From a4752266b89cc8bb5b25ac14f1b7c949f8f2b4bc Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 03:11:49 +0000 Subject: [PATCH 060/353] docs(libc): record Windows directory fopen behavior --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index beed675fdd..7b173a5be1 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -40,7 +40,7 @@ - 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. - [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 +- [`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 - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. From 837c862e22c6fccd0264a79ffd8764d882153099 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:01:07 +0530 Subject: [PATCH 061/353] feat(libkernel): implement sigpending --- .../libs/prx/libkernel/System/src/Signals.cpp | 11 ++++++++ core/libs/tests/GuestSignals.cpp | 27 ++++++++++++++++--- docs/dev/debt/libkernel.md | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 01226d305d..4629df138d 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -583,6 +583,17 @@ int APS5_VABI pause_nid_postfix(void) { } } } + +int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { + if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } + auto& pending = Pending(); + std::lock_guard lock(pending.lock); + set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); + set->bits[1] = 0; + set->bits[2] = 0; + set->bits[3] = 0; + return 0; +} } extern "C" { diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 44c0564b44..93a5311f51 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -7,6 +7,9 @@ #include #include using Handler = void (APS5_VABI *)(int); +struct GuestSignalSet { + std::uint32_t bits[4]; +}; extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); @@ -18,10 +21,8 @@ int APS5_VABI pthread_sigmask_nid_postfix(int, const void*, void*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); } -struct GuestSignalSet { - std::uint32_t bits[4]; -}; struct GuestSigaction { std::uintptr_t handler; int flags; @@ -200,4 +201,24 @@ int main() { Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == static_cast(-1) && current.flags == 0x2); Require(signal_nid_postfix(13, nullptr) == invalid); Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == 0); + *__error_nid_postfix() = 0; + Require(sigpending_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == 14); + *__error_nid_postfix() = 42; + GuestSignalSet pendingSignals{{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(*__error_nid_postfix() == 42); + Require(signal_nid_postfix(15, nullptr) == Callback); + Require(signal_nid_postfix(15, Callback) == nullptr); + received = 0; + Require(sigprocmask_nid_postfix(1, &termMask, nullptr) == 0); + Require(raise_nid_postfix(15) == 0 && received == 0); + pendingSignals = {{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == (1u << 14) && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(sigprocmask_nid_postfix(2, &termMask, nullptr) == 0 && received == 15); + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(signal_nid_postfix(15, nullptr) == Callback); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2e5574be7f..0f7851b4dd 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -124,6 +124,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported +- [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set From e2ee9430bf61a090632c88a0905a37ac73ae0061 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Sun, 11 Oct 2026 22:39:58 +0200 Subject: [PATCH 062/353] perf(agc): allocate CPU snapshots on demand --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/include/Texture.hpp | 3 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 162 ++++++++++++++++++ 4 files changed, 172 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 371ccc8c20..d5b79810e4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -916,6 +916,11 @@ if(BUILD_TESTING) set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) + set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 6dafa8345a..5051c77187 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -328,6 +328,7 @@ public: static std::uint64_t SinglePassMoves(); static std::uint64_t RefreshesProved(); std::uint64_t GuestBytes() const; + std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } private: @@ -544,7 +545,7 @@ private: bool dirty = false; std::uint64_t version = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. - bool originalValid = true; + bool originalValid = false; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 1f904ebf36..342fcd5df1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1883,6 +1883,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std } void StorageTexture::captureGuestBytes(const std::vector* layers) { + original.resize(static_cast(guestBytes)); comparedGuestBytes = {}; const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }); std::vector selected(trackedLayers, true); @@ -1942,7 +1943,6 @@ void StorageTexture::upload(const std::vector* layers) { const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const auto elementBytes = BytesPerElement(descriptor.format); const auto linearBytes = sliceLinearBytes * arrayLayers; - original.resize(static_cast(guestBytes)); // A layer selection applies to the direct path alone (the others upload everything); it names // array layers, which the tracked layers are when there are several. if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr; @@ -2143,6 +2143,7 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } + original.resize(static_cast(guestBytes)); originalValid = true; forgetBorrowed(0, trackedLayers); stampLayers(false); @@ -4269,6 +4270,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for // the content again only where it did before, or once every unit is stored. const bool wasValid = originalValid; + original.resize(static_cast(guestBytes)); if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original); DeviceBuffer linear(context, static_cast(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 9d052581aa..1c2f17b171 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5716,6 +5716,156 @@ void pipelineLibraryTests(const Device& device) { std::cout << "Pipeline library reuse tests passed\n"; } +#ifdef _WIN32 +namespace StorageCpuSnapshotTest { +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 SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::size_t BlockBytes = 12 * SurfaceBytes; + +class Block { +public: + explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) { +#ifdef _WIN32 + if (watched) { + memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot allocate the shared texture section"); + try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); } + catch (...) { CloseHandle(section); throw; } + CloseHandle(section); + } else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + memory = std::aligned_alloc(65536, BlockBytes); +#endif + Require(memory != nullptr, "cannot allocate the texture block"); + std::memset(memory, 0x5a, BlockBytes); + GuestAllocations::Mutation().Add(memory, BlockBytes, true, true); + } + + ~Block() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); + HostImportFor(context, Surface(0), BlockBytes); +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); + } else VirtualFree(memory, 0, MEM_RELEASE); +#else + std::free(memory); +#endif + } + + std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast(memory) + surface * SurfaceBytes; } + +private: + const Context& context; + bool watched; + void* memory = nullptr; +}; + +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { + 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, + }; +} + +GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) { + const auto words = Descriptor(address, side); + return DecodeTextureResource(words); +} + +void Run(Context context) { + TextureDetiler detiler(context); + context.detiler = &detiler; + for (const bool watched : {true, false}) { + Block block(context, watched); + const auto address = block.Surface(0); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking"); + Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import"); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking"); + auto image = std::make_shared(context, *context.detiler, Resource(address), 0); + const auto expectedBytes = watched ? 0u : SurfaceBytes; + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot"); + const auto readPixel = [&] { + Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, ©); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + result.Invalidate(); + std::uint32_t pixel = 0; + std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel)); + return pixel; + }; + Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels"); + std::memset(reinterpret_cast(address), 0x6b, SurfaceBytes); + image->Refresh(); + Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot"); + { + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + VkClearColorValue clear{}; + clear.uint32[0] = 0x7c7c7c7cu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + std::uint32_t pixel = 0; + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot"); + if (watched) { + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched"); + std::memset(reinterpret_cast(address), 0x8d, SurfaceBytes); + image->Refresh(); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot"); + Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels"); + } + image.reset(); + Context cpu = context; + cpu.hostImportAlignment = 0; + image = std::make_shared(cpu, *context.detiler, Resource(address), 0); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot"); + const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu; + Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels"); + { + CommandBatch batch(context); + VkClearColorValue clear{}; + clear.uint32[0] = 0x9e9e9e9eu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels"); + std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes()); + } +} +} +#endif + int main(int argc, char** argv) { try { std::unique_ptr created; @@ -5750,6 +5900,18 @@ int main(int argc, char** argv) { return 0; } std::unique_lock gpu(GpuMutex()); +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") { + if (device.GetContext().hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports"); + std::cout << "skipped, host imports unavailable\n"; + return 77; + } + StorageCpuSnapshotTest::Run(device.GetContext()); + std::cout << "Storage CPU snapshot tests passed\n"; + return 0; + } +#endif std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); From 65eb466ddde7def2840d9770c544c123bac69926 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Sun, 11 Oct 2026 23:51:10 +0200 Subject: [PATCH 063/353] fix(agc): avoid snapshot upload PR conflict --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 342fcd5df1..ff5bafb7cf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2143,8 +2143,8 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } - original.resize(static_cast(guestBytes)); originalValid = true; + original.resize(static_cast(guestBytes)); forgetBorrowed(0, trackedLayers); stampLayers(false); GuestMemory::ReadCommitted(descriptor.baseAddress, original); From a296b9cacf46406c9d5ce3ea669bcb5c8d49bf9f Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Mon, 12 Oct 2026 00:28:34 +0200 Subject: [PATCH 064/353] fix(agc): register snapshot fixture GPU memory --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 1c2f17b171..bd4fb56418 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5728,7 +5728,6 @@ constexpr std::size_t BlockBytes = 12 * SurfaceBytes; class Block { public: explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) { -#ifdef _WIN32 if (watched) { memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536); const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); @@ -5737,26 +5736,19 @@ public: catch (...) { CloseHandle(section); throw; } CloseHandle(section); } else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); -#else - memory = std::aligned_alloc(65536, BlockBytes); -#endif Require(memory != nullptr, "cannot allocate the texture block"); std::memset(memory, 0x5a, BlockBytes); - GuestAllocations::Mutation().Add(memory, BlockBytes, true, true); + GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true); } ~Block() { ClearCachedTextures(context.device); GuestAllocations::Mutation().Remove(memory); HostImportFor(context, Surface(0), BlockBytes); -#ifdef _WIN32 if (watched) { GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); } else VirtualFree(memory, 0, MEM_RELEASE); -#else - std::free(memory); -#endif } std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast(memory) + surface * SurfaceBytes; } From 3f314030b4728581a609b5621f9b2133cf4480ba Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Mon, 12 Oct 2026 23:43:29 +0200 Subject: [PATCH 065/353] fix(agc): use storage factory in snapshot test --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index bd4fb56418..ed3a4bbd54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5786,7 +5786,7 @@ void Run(Context context) { Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking"); Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import"); Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking"); - auto image = std::make_shared(context, *context.detiler, Resource(address), 0); + auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0); const auto expectedBytes = watched ? 0u : SurfaceBytes; Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot"); const auto readPixel = [&] { @@ -5835,7 +5835,7 @@ void Run(Context context) { image.reset(); Context cpu = context; cpu.hostImportAlignment = 0; - image = std::make_shared(cpu, *context.detiler, Resource(address), 0); + image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0); Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot"); const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu; Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels"); From 723d02c6b89ab7ba404ce240710fdf6166167bb9 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:17:25 +0200 Subject: [PATCH 066/353] fix(relinker): relocate Windows guest TLS from its thread pointer --- .../src/windows/WindowsGuestModuleWriter.cpp | 4 +- .../relinker/tests/guest_import_fixtures.py | 35 +++++++++++++ .../relinker/relinker/tests/test_guest_tls.py | 51 ++++++++++++++++++- .../tests/test_windows_import_modules.py | 34 +------------ 4 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 core/relinker/relinker/tests/guest_import_fixtures.py diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index bcc3469ddc..6faebae1ea 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -58,8 +58,8 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else if (type == 18) { const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); if (tls == guest.Headers.end() || (symbolIndex != 0 && (symbol.Section == 0 || (symbol.Info & 15) != 6))) throw Domain::RelinkerException("Unsupported external static TLS relocation", target); - const auto size = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 16)); - image.WritePointer(target, symbol.Value + addend - size); + const auto threadPointerOffset = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 1)); + image.WritePointer(target, symbol.Value + addend - threadPointerOffset); } else if (type == 1 || type == 6 || type == 7) { if (symbolIndex == 0 || (type != 1 && addend != 0) || (symbol.Info & 15) == 6) throw Domain::RelinkerException("Invalid guest symbol relocation", target); if (symbol.Section == Relinker::AbsoluteSection) image.WritePointer(target, symbol.Value + addend); diff --git a/core/relinker/relinker/tests/guest_import_fixtures.py b/core/relinker/relinker/tests/guest_import_fixtures.py new file mode 100644 index 0000000000..ac961cecf7 --- /dev/null +++ b/core/relinker/relinker/tests/guest_import_fixtures.py @@ -0,0 +1,35 @@ +import struct + +from test_guest_intel_trampolines import main_fixture + + +def dynamic(image, offset, phoff, tags): + for index, tag in enumerate(tags + [(0, 0)]): + struct.pack_into(' Date: Tue, 13 Oct 2026 11:11:58 +0200 Subject: [PATCH 067/353] fix(agc): avoid storage snapshot overlap Create uploads before publishing snapshot validity. Keep allocation edits separate from the per-unit validity changes in #5024. --- core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp | 2 +- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5051c77187..2ad97c3379 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -545,7 +545,7 @@ private: bool dirty = false; std::uint64_t version = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. - bool originalValid = false; + bool originalValid = true; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ff5bafb7cf..6704ead777 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2144,9 +2144,9 @@ void StorageTexture::upload(const std::vector* layers) { return; } originalValid = true; - original.resize(static_cast(guestBytes)); forgetBorrowed(0, trackedLayers); stampLayers(false); + original.resize(static_cast(guestBytes)); GuestMemory::ReadCommitted(descriptor.baseAddress, original); { Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -4265,12 +4265,12 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } return; } + original.resize(static_cast(guestBytes)); // The store below compares against the guest bytes, read now when `original` does not hold // them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a // stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for // the content again only where it did before, or once every unit is stored. const bool wasValid = originalValid; - original.resize(static_cast(guestBytes)); if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original); DeviceBuffer linear(context, static_cast(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); From e10d7f1a841b5b5dc22a04a8f836ca277aa13576 Mon Sep 17 00:00:00 2001 From: mariobob <13618350+mariobob@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:04:48 +0200 Subject: [PATCH 068/353] fix(kernel): build the param.json parser with libc++ before macOS 26 Apple's libc++ marks floating-point std::from_chars unavailable below a macOS 26 deployment target, so the relinker-only build fails on macOS 15 since the JSON number conversion switched to it. Use from_chars where the standard library defines __cpp_lib_to_chars and otherwise convert through a stream imbued with the classic locale, which keeps the conversion independent of the host locale and keeps the full-consumption and range checks. --- .../AppMetadata/src/ParamJsonParser.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 585b732a1d..901c20e69b 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -206,6 +207,21 @@ JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::si return value; } +double parseJsonNumberValue(const std::string& number) { + double value = 0.0; +#ifdef __cpp_lib_to_chars + const auto converted = std::from_chars(number.data(), number.data() + number.size(), value); + const bool parsed = converted.ec == std::errc{} && converted.ptr == number.data() + number.size(); +#else + std::istringstream stream(number); + stream.imbue(std::locale::classic()); + stream >> value; + const bool parsed = !stream.fail() && stream.eof(); +#endif + if (!parsed) throw std::runtime_error("number outside supported range in param.json"); + return value; +} + JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { const std::size_t start = position; if (peekChar(text, position) == '-') ++position; @@ -230,10 +246,7 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { JsonValue value; value.type = JsonType::Number; value.numberText = text.substr(start, position - start); - const auto& number = value.numberText; - const auto converted = std::from_chars(number.data(), number.data() + number.size(), value.numberValue); - if (converted.ec != std::errc{} || converted.ptr != number.data() + number.size()) - throw std::runtime_error("number outside supported range in param.json"); + value.numberValue = parseJsonNumberValue(value.numberText); return value; } From bde680c855263f27d7c93152961b0de0bab13142 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:40:28 +0200 Subject: [PATCH 069/353] fix(relinker): map standalone RELRO in unused import filtering --- .../UnusedNidFilter/EntryPointCollector.cpp | 9 +++--- .../RelativeRelocationIndex.cpp | 11 +++---- .../relinker/tests/StrictNidFilterTests.cpp | 2 ++ .../relinker/relinker/tests/test_sce_relro.py | 32 +++++++++++++++++++ docs/dev/debt/relinker.md | 2 +- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index 8e87a10d8e..e95afa383b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -75,11 +76,9 @@ public: if (phEntSize < 56) throw RelinkerException("ELF program header entry too small"); std::vector loads; - for (std::uint16_t i = 0; i < phCount; ++i) { - std::size_t phPos = static_cast(phOff) + i * phEntSize; - if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); - if (read32(elfBytes, phPos) != PT_LOAD) continue; - loads.push_back({read64(elfBytes, phPos + 16), read64(elfBytes, phPos + 8), read64(elfBytes, phPos + 32)}); + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } for (std::uint16_t i = 0; i < phCount; ++i) { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 39de6fdf66..1d0be749bc 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -147,12 +148,10 @@ std::unique_ptr BuildRelativeRelocationIndex( hasSceDynlibData = true; sceTableBase = read64(elfBytes, phPos + 8); } - if (type != PT_LOAD) continue; - LoadSegment seg; - seg.fileOffset = read64(elfBytes, phPos + 8); - seg.vaddr = read64(elfBytes, phPos + 16); - seg.fileSize = read64(elfBytes, phPos + 32); - loads.push_back(seg); + } + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } std::unordered_map targets; diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index 4878395603..b39ffb6c90 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -397,7 +397,9 @@ void relativeRelocationTableBounds() { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); diff --git a/core/relinker/relinker/tests/test_sce_relro.py b/core/relinker/relinker/tests/test_sce_relro.py index bcb8783375..51de2a0953 100644 --- a/core/relinker/relinker/tests/test_sce_relro.py +++ b/core/relinker/relinker/tests/test_sce_relro.py @@ -1,3 +1,4 @@ +import json import os from pathlib import Path import struct @@ -41,6 +42,29 @@ def relink(relinker, work, name, image, options): return result, output +def filtered_fixture(extra, lifecycle): + image = fixture(extra) + if not lifecycle: + image[0x1100:0x1118] = image[0x700:0x718] + struct.pack_into(" Date: Tue, 13 Oct 2026 11:09:21 +0200 Subject: [PATCH 070/353] fix(libScePlayGo): answer chunk ids from the package layout A package with no playgo-* file in sce_sys has no PlayGo data, so only chunk 0 is valid and any other id returns SCE_PLAYGO_ERROR_BAD_CHUNK_ID. A package with playgo-scenario.json but no chunk table (PPSA01748) keeps accepting every id. A chunk table (playgo-chunk.dat or playgo-chunkdefs.xml) still decides when it exists. A sce_sys that holds other playgo-* files without a chunk table or a scenario file, or a playgo-chunk.dat that is not a plgx table with a nonzero count, throws naming the file, since no such dump is known. Without this a title that walks the ids until the first error (Sifu PPSA03001) never gets the error. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libScePlayGo/Export.cpp | 25 +++++- core/libs/tests/GuestPlayGo.cpp | 110 +++++++++++++++++++++----- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 111 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index bf70cc09d6..64420b32d3 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -3,10 +3,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include #include +#include #include // The game is fully installed on the host, so every chunk is local and nothing is pending. @@ -28,27 +30,42 @@ static constexpr int SCE_PLAYGO_ERROR_BAD_LOCUS = static_cast(0x80B20010); struct ChunkTable { std::set ids{0}; bool defined = false; + bool scenario = false; }; static bool ReadPackageChunks(ChunkTable& table) { constexpr std::size_t CountOffset = 0x0A; std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); char header[CountOffset + sizeof(uint16_t)]{}; - if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) return false; + if (!file) return false; + const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx header with a nonzero chunk count)"); }; + if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) throw unreadable(); uint16_t count = 0; std::memcpy(&count, header + CountOffset, sizeof(count)); - if (count == 0) return false; + if (count == 0) throw unreadable(); for (uint32_t id = 0; id < count; ++id) table.ids.insert(static_cast(id)); table.defined = true; return true; } +static void RejectUnreadablePlayGoFiles() { + std::error_code error; + const std::filesystem::path directory = ResolvePath_nid_no_patch("/app0/sce_sys"); + for (const auto& entry : std::filesystem::directory_iterator(directory, error)) { + const auto name = entry.path().filename().string(); + if (name.rfind("playgo-", 0) == 0) + throw std::runtime_error("scePlayGo: sce_sys/" + name + " is present but no chunk table (playgo-chunk.dat) or scenario file (playgo-scenario.json) is, so the chunk ids are unknown"); + } +} + static const ChunkTable& Chunks() { static const ChunkTable chunks = [] { ChunkTable table; std::ifstream file(ResolvePath_nid_no_patch("/app0/playgo-chunkdefs.xml")); if (!file) { - ReadPackageChunks(table); + if (ReadPackageChunks(table)) return table; + table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); + if (!table.scenario) RejectUnreadablePlayGoFiles(); return table; } table.defined = true; @@ -70,7 +87,7 @@ static const ChunkTable& Chunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - return !chunks.defined || chunks.ids.contains(id); + return chunks.defined ? chunks.ids.contains(id) : chunks.scenario || id == 0; } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 2cea5041e1..cf93ef9eb2 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); @@ -39,31 +42,30 @@ static int CheckPackageChunkTable(const char* root) { return 0; } -static void RunWithPackageChunkTable(const char* self) { - const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); - std::filesystem::remove_all(root); - std::filesystem::create_directories(root / "app0" / "sce_sys"); - char header[0x100] = {'p', 'l', 'g', 'x'}; - header[0x0A] = 3; - std::ofstream(root / "app0" / "sce_sys" / "playgo-chunk.dat", std::ios::binary).write(header, sizeof(header)); - const std::string arguments = "\"" + std::string(self) + "\" --package-chunk-table \"" + root.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - const int status = std::system(command.c_str()); - std::filesystem::remove_all(root); - Require(status == 0); +static int CheckScenarioWithoutChunkTable(const char* root) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + std::int8_t loci[3] = {0, 0, 0}; + Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoClose(handle) == 0); + return 0; } -int main(int argc, char** argv) { - if (argc == 3 && std::string_view(argv[1]) == "--package-chunk-table") return CheckPackageChunkTable(argv[2]); - RunWithPackageChunkTable(argv[0]); +static int CheckNoPlayGoFile(const char* root) { + std::filesystem::current_path(root); constexpr int badHandle = static_cast(0x80B20009); constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); constexpr int badLocus = static_cast(0x80B20010); + constexpr int badChunkId = static_cast(0x80B2000C); PlayGoInitParams init{}; int handle = 0; @@ -84,8 +86,10 @@ int main(int argc, char** argv) { Require(scePlayGoSetToDoList(handle, todo, 2) == 0); todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); + todo[1] = {1, 3, 0}; + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); todo[1] = {0xFFFF, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); std::uint32_t entries = 1; Require(scePlayGoGetToDoList(handle, todo, 2, &entries) == 0 && entries == 0); @@ -95,9 +99,73 @@ int main(int argc, char** argv) { Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; - Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == badChunkId); + const std::uint16_t onlyDeclared[2] = {0, 0}; + Require(scePlayGoGetLocus(handle, onlyDeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); Require(scePlayGoClose(handle + 1) == badHandle); Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckRejectedLayout(const char* root, const char* fileName) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t chunk = 0; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &chunk, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find(fileName) != std::string_view::npos; + } + Require(threw); + return 0; +} + +static void RunLayout(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) std::ofstream(root / "app0" / "sce_sys" / name, std::ios::binary).write(content.data(), static_cast(content.size())); + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +static std::string ChunkTableFile() { + std::string header(0x100, '\0'); + header.replace(0, 4, "plgx"); + header[0x0A] = 3; + return header; +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-file") return CheckNoPlayGoFile(argv[2]); + if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); + if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); + if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); + if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); + if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); + } + RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); + RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); + RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); + RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); + return 0; } diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 3a13df5e03..ecf89fb032 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file every chunk id is accepted while `scePlayGoGetChunkId` lists only chunk 0; the console's answer for an id outside the package's chunk table is not checked. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0, and a package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts only chunk 0 and returns `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` for any other id; a `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known; the two answers above are unverified on a console, as is the console's answer for an id outside a package's chunk table. From a1d19f00e480650a9147d5381c15557f9d73f408 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Tue, 13 Oct 2026 12:30:53 +0200 Subject: [PATCH 071/353] fix(recompiler): give vertex-path shaders the NGG group info The vertex entry set the merged wave info in s3 but left s2 at 0. On the NGG path s2 holds GS_TG_INFO, the subgroup's vertex count in bits 12-20 and primitive count in bits 22-30. LLPC-compiled shaders build EXEC from it (v_cmp_gt_u32 vcc, , ), so with 0 no vertex was exported and every draw rasterized nothing. EVO Player, Jelly5 and Emby5 presented only the CPU clear colour. Set s2 to the same lane count as both vertex and primitive count, with ordered wave ID 0, alongside s3. Fixes #4997 Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/NggVertexWaveInfo.cpp | 23 ++++++++++++------- .../Translation/src/InstructionTranslator.cpp | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp index ad361921fd..1fc52d68ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp @@ -24,6 +24,11 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; +alignas(256) constexpr std::array GroupInfoVertexCode{ + 0x9380ff02, 0x0009000c, 0x8f009000, 0x94fe00c1, + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, }; @@ -37,16 +42,16 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span vertexCode) { Pixels.fill(Kept); const auto target = device.Target(); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {waveSize, 8, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} @@ -97,12 +102,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { device.WaitIdle(); } -void Check(std::uint32_t waveSize) { - const auto what = "NGG vertex prologue wave" + std::to_string(waveSize); +void Check(std::uint32_t waveSize, const std::string& source) { + const auto what = "NGG vertex prologue wave" + std::to_string(waveSize) + " (" + source + ")"; for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4; ++channel) { const auto actual = Pixels[pixel * 4u + channel]; - Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the merged wave info must cover every vertex)"); + Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the " + source + " must cover every vertex)"); } } } @@ -118,8 +123,10 @@ int main() { return VulkanTestSkipped; } for (const auto waveSize : {64u, 32u}) { - Draw(*device, waveSize); - Check(waveSize); + Draw(*device, waveSize, VertexCode); + Check(waveSize, "merged wave info"); + Draw(*device, waveSize, GroupInfoVertexCode); + Check(waveSize, "group info"); } std::puts("NGG vertex wave info tests passed"); return 0; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 247197f835..b5a0834919 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -378,6 +378,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO if (options.userDataBaseRegister >= 8u) { const auto hostLanes = options.inputInfo.vertex->hostSubgroupSize; const std::uint32_t lanes = hostLanes == 1u ? 1u : options.waveSize == 64u && hostLanes == 32u ? 32u : options.waveSize; + entryIr.SetScalarReg(static_cast(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); From 485f638a530e828109f623f4920fb976b462f449 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Tue, 13 Oct 2026 16:03:16 +0530 Subject: [PATCH 072/353] fix(ajm): accept Opus mapping family 255 for a mono stream Co-authored-by: poonasor <12569806+poonasor@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 2 +- core/libs/tests/GuestAjm.cpp | 48 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 8dc5f2ad78..15cc83db67 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -374,7 +374,7 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::uint32_t mappingFamily = 0; if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); - if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); + if (mappingFamily > 1 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index b03e0a14fb..5d0dc75362 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -496,6 +496,53 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void TestOpusMonoFamily255(std::uint32_t context) { + std::uint32_t instances[2] = {}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, &instances[index]) == 0); + const std::uint32_t parameters[3] = {1, 48000, index == 0 ? 0u : 255u}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instances[index], parameters, sizeof(parameters), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + std::size_t offset = 0; + int peak = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + std::vector pcm[2] = {std::vector(960, -12345), std::vector(960, -12345)}; + for (std::size_t index = 0; index < 2; ++index) { + DecodeSideband sideband{}; + RunDecode(context, instances[index], OPUS_STEREO + offset, 2 + bytes, pcm[index].data(), pcm[index].size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2); + Require(sideband.totalDecodedSamples == static_cast(packet + 1) * 960); + } + Require(pcm[0] == pcm[1]); + if (packet > 0) { + for (const std::int16_t sample : pcm[1]) peak = std::max(peak, std::abs(static_cast(sample))); + } + offset += 2 + bytes; + } + Require(peak > 900 && peak < 1200); + + const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + for (const auto& parameters : refusedParameters) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + } + for (const std::uint32_t instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestDecodeSingle(std::uint32_t context) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -1245,6 +1292,7 @@ int main() { TestMp3(context); TestOpus(context); TestOpus(context, 1); + TestOpusMonoFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 3a3e8f5009..d9a2f57845 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts -- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. Only 48000 Hz and mapping families 0 and 1 are implemented; other rates and families throw +- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does From 4f87ac0b1ccf495de912de7a75c416606eaf00db Mon Sep 17 00:00:00 2001 From: Yaash Singhh Date: Tue, 13 Oct 2026 10:40:44 +0000 Subject: [PATCH 073/353] fix(libkernel): harden DirectMemory pool bounds and rollback --- core/libs/CMakeLists.txt | 10 + .../prx/libkernel/DirectMemory/MemoryPool.cpp | 283 ++++++++++----- core/libs/tests/MemoryPoolTest.cpp | 341 ++++++++++++++++++ 3 files changed, 549 insertions(+), 85 deletions(-) create mode 100644 core/libs/tests/MemoryPoolTest.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 07e9890094..98b13ff394 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -411,6 +411,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(MemoryPoolTest + tests/MemoryPoolTest.cpp + prx/libkernel/DirectMemory/MemoryPool.cpp + ) + target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(MINGW) + target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index d7c6c65d76..8fa22b5fbc 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,10 +1,54 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include +#include +#include #include #include +#include +#include +#include -static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; +#include "prx/libkernel/KernelErrors.hpp" + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPoolBytes = static_cast(DIRECT_MEMORY_SIZE); +constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); + +static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); +static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); + +bool RoundUp(uint64_t v, uint64_t a, uint64_t& out) { + const uint64_t r = v % a; + if (r == 0) { + out = v; + return true; + } + if (v > UINT64_MAX - (a - r)) return false; + out = v + (a - r); + return true; +} + +bool NormalizeAlignment(size_t alignment, uint64_t& out) { + return RoundUp(std::max(static_cast(alignment), kPage), kPage, out); +} + +bool NormalizeLength(size_t len, uint64_t& out) { + uint64_t rounded; + if (!RoundUp(static_cast(len), kPage, rounded) || rounded > kPoolBytes) return false; + out = rounded; + return true; +} + +bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { + if (len == 0 || start >= kPoolBytes || len > kPoolBytes - start) return false; + lo = start & ~(kPage - 1); + hi = (start + len + kPage - 1) & ~(kPage - 1); + return true; +} struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { @@ -13,87 +57,98 @@ struct PhysicalMemoryPool { } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + if (len == 0) return SCE_KERNEL_ERROR_EINVAL; + + uint64_t align, alignedLen; + if (!NormalizeAlignment(alignment, align) || !NormalizeLength(len, alignedLen)) return SCE_KERNEL_ERROR_EAGAIN; + + const uint64_t start = static_cast(searchStart); + const uint64_t end = std::min(static_cast(searchEnd), kPoolBytes); + uint64_t cur; + if (start >= end || !RoundUp(start, align, cur)) return SCE_KERNEL_ERROR_EAGAIN; + std::lock_guard lock(_mutex); - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - uint64_t start = static_cast(searchStart); - uint64_t end = static_cast(searchEnd); - uint64_t cur = (start + align - 1) & ~(align - 1); - while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { - if (_isFree(cur, len)) { - CreateDirectMemoryBacking(static_cast(cur), len, memoryType); - _mark(cur, len, true); - _blocks[cur] = {cur + len, memoryType}; + const size_t pageCount = static_cast(alignedLen / kPage); + + while (cur <= end && alignedLen <= end - cur) { + size_t lastUsed; + if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { + _commit(cur, alignedLen, memoryType); *physOut = static_cast(cur); return 0; } - cur += align; + if (!RoundUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } return SCE_KERNEL_ERROR_EAGAIN; } - void Free(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - _free(start, len); - } + bool Free(uint64_t start, size_t len) { return _release(start, len, false); } - bool CheckedFree(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; - const size_t first = start / PS5_PAGE_SIZE; - for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) - if (!_used[first + i]) return false; - _free(start, len); - return true; - } + bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { + if (!start || !end || !memoryType || offset >= kPoolBytes) return false; std::lock_guard lock(_mutex); - auto it = _blocks.upper_bound(offset); + BlockMap::const_iterator it = _blocks.upper_bound(offset); if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); *end = static_cast(it->second.end); *memoryType = it->second.type; return true; } - void Retype(uint64_t start, size_t len, int memoryType) { + bool Retype(uint64_t start, size_t len, int memoryType) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + std::lock_guard lock(_mutex); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + const bool splitLo = _splitAt(lo); + try { + _splitAt(hi); + } catch (...) { + if (splitLo) _unsplitAt(lo); + throw; } + for (BlockMap::iterator it = _blocks.lower_bound(lo); it != _blocks.end() && it->first < hi; ++it) { + it->second.type = memoryType; + } + return true; } bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - std::lock_guard lock(_mutex); + if (!startOut || !sizeOut) return false; if (searchStart < 0 || searchEnd <= searchStart) return false; - const uint64_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - const uint64_t limit = std::min(static_cast(searchEnd), DIRECT_MEMORY_SIZE); + uint64_t align; + if (!NormalizeAlignment(alignment, align)) return false; + + uint64_t cur; + if (!RoundUp(static_cast(searchStart), kPage, cur)) return false; + const uint64_t limit = std::min(static_cast(searchEnd), kPoolBytes) & ~(kPage - 1); + + std::lock_guard lock(_mutex); uint64_t bestStart = 0; uint64_t bestSize = 0; - uint64_t cur = (static_cast(searchStart) + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - while (cur + PS5_PAGE_SIZE <= limit) { - if (!_isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; + + while (cur < limit) { + if (_used[cur / kPage]) { + cur += kPage; continue; } uint64_t runEnd = cur; - while (runEnd + PS5_PAGE_SIZE <= limit && _isFree(runEnd, PS5_PAGE_SIZE)) runEnd += PS5_PAGE_SIZE; - const uint64_t alignedStart = (cur + align - 1) & ~(align - 1); + while (runEnd < limit && !_used[runEnd / kPage]) runEnd += kPage; + + uint64_t alignedStart; + if (!RoundUp(cur, align, alignedStart)) break; if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { bestStart = alignedStart; bestSize = runEnd - alignedStart; } cur = runEnd; } + if (bestSize == 0) return false; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); @@ -101,64 +156,122 @@ struct PhysicalMemoryPool { } size_t FreeRun(uint64_t offset, uint64_t limit) { + const uint64_t lim = std::min(limit, kPoolBytes); + if (offset >= lim) return 0; + std::lock_guard lock(_mutex); - uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); + uint64_t cur = offset & ~(kPage - 1); size_t run = 0; - while (cur + PS5_PAGE_SIZE <= limit && cur + PS5_PAGE_SIZE <= DIRECT_MEMORY_SIZE - && _isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; + while (cur <= lim && kPage <= lim - cur && !_used[cur / kPage]) { + cur += kPage; + run += static_cast(kPage); } return run; } private: - void _free(uint64_t start, size_t len) { - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; - } - } - - bool _isFree(uint64_t offset, size_t len) const { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - if (_used[first + i]) return false; - return true; - } - - void _mark(uint64_t offset, size_t len, bool used) { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - _used[first + i] = used; - } - struct Block { uint64_t end; int type; }; + typedef std::map BlockMap; + + bool _lastUsedPage(size_t first, size_t count, size_t& out) const { + for (size_t i = count; i-- > 0;) { + if (_used[first + i]) { + out = first + i; + return true; + } + } + return false; + } + + bool _allUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (!_used[first + i]) return false; + } + return true; + } + + void _mark(size_t first, size_t count, bool used) { + for (size_t i = 0; i < count; ++i) _used[first + i] = used; + } + + void _commit(uint64_t start, uint64_t len, int type) { + std::pair ins = _blocks.emplace(start, Block{start + len, type}); + if (!ins.second) throw std::logic_error("DirectMemory pool: block map and page flags disagree"); + + try { + CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); + } catch (...) { + _blocks.erase(ins.first); + throw; + } + + _mark(static_cast(start / kPage), static_cast(len / kPage), true); + } + + bool _release(uint64_t start, size_t len, bool requireAllocated) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + const size_t first = static_cast(lo / kPage); + const size_t count = static_cast((hi - lo) / kPage); + + std::lock_guard lock(_mutex); + if (requireAllocated && !_allUsed(first, count)) return false; + + const bool splitLo = _splitAt(lo); + bool splitHi = false; + try { + splitHi = _splitAt(hi); + ForgetDirectMemory(static_cast(lo), static_cast(hi - lo)); + } catch (...) { + if (splitHi) _unsplitAt(hi); + if (splitLo) _unsplitAt(lo); + throw; + } + + _blocks.erase(_blocks.lower_bound(lo), _blocks.lower_bound(hi)); + _mark(first, count, false); + return true; + } + + bool _splitAt(uint64_t at) { + BlockMap::iterator it = _blocks.upper_bound(at); + if (it == _blocks.begin()) return false; + --it; + if (it->first == at || it->second.end <= at) return false; + + const Block tail = {it->second.end, it->second.type}; + _blocks.emplace(at, tail); + it->second.end = at; + return true; + } + + void _unsplitAt(uint64_t at) noexcept { + BlockMap::iterator right = _blocks.find(at); + BlockMap::iterator left = std::prev(right); + left->second.end = right->second.end; + _blocks.erase(right); + } std::mutex _mutex; - bool _used[NUM_PAGES] = {}; - std::map _blocks; + bool _used[kNumPages] = {}; + BlockMap _blocks; }; +} + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + try { + return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } } void DirectMemoryFree(int64_t start, size_t len) { - PhysicalMemoryPool::Instance().Free(static_cast(start), len); + (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { @@ -170,7 +283,7 @@ bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* en } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { - PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); + (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { diff --git a/core/libs/tests/MemoryPoolTest.cpp b/core/libs/tests/MemoryPoolTest.cpp new file mode 100644 index 0000000000..4134c904a7 --- /dev/null +++ b/core/libs/tests/MemoryPoolTest.cpp @@ -0,0 +1,341 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +bool DirectMemoryCheckedFree(int64_t start, size_t len); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); +size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPool = static_cast(DIRECT_MEMORY_SIZE); + +enum class Fault { None, BackingBadAlloc, BackingRuntimeError, ForgetRuntimeError }; +Fault g_fault = Fault::None; +long g_newBudget = -1; + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf(" FAILED line %d: %s\n", __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +struct NewBudget { + explicit NewBudget(long n) { g_newBudget = n; } + ~NewBudget() { g_newBudget = -1; } +}; + +bool BlockAt(uint64_t offset, int64_t& s, int64_t& e, int& t) { + return DirectMemoryFind(static_cast(offset), false, &s, &e, &t); +} + +bool BlockIs(uint64_t offset, uint64_t start, uint64_t end, int type) { + int64_t s = 0, e = 0; + int t = 0; + return BlockAt(offset, s, e, t) && s == static_cast(start) && e == static_cast(end) && t == type; +} + +bool PoolEmpty() { + int64_t s, e; + int t; + return !DirectMemoryFind(0, true, &s, &e, &t) && DirectMemoryFreeRun(0, kPool) == kPool; +} + +void alloc_unplaceable_requests() { + int64_t out = -1; + CHECK(DirectMemoryAlloc(0, kPool, 0, 0, 1, &out) == SCE_KERNEL_ERROR_EINVAL); + CHECK(DirectMemoryAlloc(-1, kPool, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, SIZE_MAX, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, kPool + kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(2 * kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPool, kPool + kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(out == -1); + CHECK(PoolEmpty()); +} + +void alloc_null_out() { + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + CHECK(PoolEmpty()); +} + +void alloc_rounding_alignment_and_clamping() { + int64_t a = -1, b = -1; + CHECK(DirectMemoryAlloc(0, kPool, 1, 1, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, -1, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + CHECK(DirectMemoryAlloc(0, INT64_MAX, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + + CHECK(DirectMemoryAlloc(1, kPool, kPage, 0, 1, &a) == 0 && a == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &b) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_non_power_of_two_alignment() { + int64_t a = -1, b = -1, c = -1; + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &a) == 0 && a == static_cast(3 * kPage)); + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &b) == 0 && b == static_cast(6 * kPage)); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 3 * kPage, 1, &c) == 0 && c == 0); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 1)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_exhaustion_and_gap_reuse() { + int64_t a, b, c; + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(2 * kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(static_cast(kPage), kPage); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(kPage)); + DirectMemoryFree(0, kPool); +} + +void free_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryFree(0, SIZE_MAX); + DirectMemoryFree(static_cast(kPool), kPage); + DirectMemoryFree(static_cast(kPool - kPage), 2 * kPage); + DirectMemoryFree(INT64_MAX, kPage); + CHECK(!DirectMemoryCheckedFree(0, SIZE_MAX)); + CHECK(!DirectMemoryCheckedFree(static_cast(kPool), kPage)); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryRetype(0, SIZE_MAX, 9); + DirectMemoryRetype(static_cast(kPool), kPage, 9); + DirectMemoryRetype(static_cast(kPool - kPage), 2 * kPage, 9); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); +} + +void free_partial_and_spanning() { + int64_t a, b; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 2, &b) == 0 && b == static_cast(4 * kPage)); + DirectMemoryFree(static_cast(2 * kPage), 4 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 1)); + CHECK(BlockIs(6 * kPage, 6 * kPage, 8 * kPage, 2)); + CHECK(DirectMemoryFreeRun(2 * kPage, kPool) == 4 * kPage); + CHECK(!DirectMemoryCheckedFree(0, 4 * kPage)); + CHECK(DirectMemoryCheckedFree(0, 2 * kPage)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_splits_and_skips_gaps() { + int64_t a; + CHECK(DirectMemoryAlloc(2 * kPage, kPool, 4 * kPage, 0, 1, &a) == 0 && a == static_cast(2 * kPage)); + DirectMemoryRetype(static_cast(3 * kPage), kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 1)); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 9)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 6 * kPage, 1)); + DirectMemoryRetype(0, 8 * kPage, 5); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 5)); + CHECK(BlockIs(5 * kPage, 4 * kPage, 6 * kPage, 5)); + int64_t s, e; + int t; + CHECK(!BlockAt(0, s, e, t) && !BlockAt(6 * kPage, s, e, t)); + DirectMemoryFree(0, kPool); +} + +void alloc_backing_bad_alloc_rolls_back() { + int64_t out = -1; + g_fault = Fault::BackingBadAlloc; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + g_fault = Fault::None; + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_node_bad_alloc_rolls_back() { + int64_t out = -1; + { + NewBudget budget(0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + } + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_unexpected_exception_propagates() { + int64_t out = -1; + g_fault = Fault::BackingRuntimeError; + bool threw = false; + try { + (void)DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out); + } catch (const std::runtime_error&) { + threw = true; + } + g_fault = Fault::None; + CHECK(threw); + CHECK(PoolEmpty()); +} + +void free_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 0); + } + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 3)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 2 * kPage); + DirectMemoryFree(0, kPool); +} + +void retype_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + } + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 4 * kPage, 9)); + DirectMemoryFree(0, kPool); +} + +void forget_exception_propagates_and_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + g_fault = Fault::ForgetRuntimeError; + bool threwFree = false, threwChecked = false; + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwFree = true; + } + try { + (void)DirectMemoryCheckedFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwChecked = true; + } + g_fault = Fault::None; + CHECK(threwFree); + CHECK(threwChecked); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +struct Case { + const char* name; + void (*fn)(); +}; + +const Case kCases[] = { + {"alloc_unplaceable_requests", alloc_unplaceable_requests}, + {"alloc_null_out", alloc_null_out}, + {"alloc_rounding_alignment_and_clamping", alloc_rounding_alignment_and_clamping}, + {"alloc_non_power_of_two_alignment", alloc_non_power_of_two_alignment}, + {"alloc_exhaustion_and_gap_reuse", alloc_exhaustion_and_gap_reuse}, + {"free_out_of_range_is_ignored", free_out_of_range_is_ignored}, + {"retype_out_of_range_is_ignored", retype_out_of_range_is_ignored}, + {"free_partial_and_spanning", free_partial_and_spanning}, + {"retype_splits_and_skips_gaps", retype_splits_and_skips_gaps}, + {"alloc_backing_bad_alloc_rolls_back", alloc_backing_bad_alloc_rolls_back}, + {"alloc_node_bad_alloc_rolls_back", alloc_node_bad_alloc_rolls_back}, + {"alloc_unexpected_exception_propagates", alloc_unexpected_exception_propagates}, + {"free_node_bad_alloc_rolls_back", free_node_bad_alloc_rolls_back}, + {"retype_node_bad_alloc_rolls_back", retype_node_bad_alloc_rolls_back}, + {"forget_exception_propagates_and_rolls_back", forget_exception_propagates_and_rolls_back}, +}; + +} + +void CreateDirectMemoryBacking(int64_t, size_t, int) { + if (g_fault == Fault::BackingBadAlloc) throw std::bad_alloc(); + if (g_fault == Fault::BackingRuntimeError) throw std::runtime_error("backing"); +} + +void ForgetDirectMemory(int64_t, size_t) { + if (g_fault == Fault::ForgetRuntimeError) throw std::runtime_error("forget"); +} + +void* operator new(size_t n) { + if (g_newBudget == 0) throw std::bad_alloc(); + if (g_newBudget > 0) --g_newBudget; + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--list") == 0) { + for (const Case& c : kCases) std::printf("%s\n", c.name); + return 0; + } + int ran = 0; + for (const Case& c : kCases) { + if (argc == 2 && std::strcmp(argv[1], c.name) != 0) continue; + std::printf("[ RUN ] %s\n", c.name); + const int before = g_failures; + try { + c.fn(); + } catch (...) { + std::printf(" FAILED: unexpected exception escaped\n"); + ++g_failures; + } + std::printf("[ %s ] %s\n", g_failures == before ? " OK " : "FAIL", c.name); + ++ran; + } + if (ran == 0) { + std::printf("no such case\n"); + return 2; + } + return g_failures == 0 ? 0 : 1; +} From d4b59ce9fa89b54ab5a0e9ed34f902892d2e4f02 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:19:51 +0200 Subject: [PATCH 074/353] fix(agc): accept inactive vertex point size on non-point draws --- .../libSceAgcDriver/Graphics/src/State.cpp | 26 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 96 +++++++++++++ .../execution/InactiveVertexPointSize.cpp | 132 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 404f0ccef4..86be0e9740 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -100,6 +100,28 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt // DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict. constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u); constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u); + +std::uint32_t inactiveVertexPointSize(const QueueState& queue) { + const auto stages = find(queue.context, 0x2d5); + const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); + const auto raster = find(queue.context, 0x205); + if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0; + if ((stages->second & 0x24u) != 0) return 0; + if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0; + switch (primitive->second) { + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 17: + return 1u << 16u; + default: + return 0; + } +} + constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. @@ -694,7 +716,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } APS5_LOG_OUT_DEBUG("Topology=%u", static_cast(result.topology)); result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; - zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); result.samples = decodeSamples(cx); @@ -1235,7 +1257,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6338e24625..d93e5b32d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -99,6 +99,90 @@ void expectFailure(TAction action, std::string_view reason) { throw std::runtime_error("expected graphics rejection: " + std::string(reason)); } +void inactiveVertexPointSizeTests() { + const auto pointSizeState = [] { + auto queue = makeState(); + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = 0x01210000; + return queue; + }; + for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + for (const auto raster : {0u, 0x240u, 0x248u}) { + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } + } + for (const auto raster : {0u, 0x240u, 0x248u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = 3; + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true); + Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection); + const auto indexedState = AgcDriver::Graphics::DecodeState(queue); + Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding"); + } + const auto rejected = [](AgcDriver::QueueState queue) { + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck"); + try { + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("potentially active point size passed the state decoder"); + }; + for (const auto primitive : {1u, 9u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + rejected(queue); + } + for (const auto stages : {0x2004u, 0x2020u}) { + auto queue = pointSizeState(); + queue.context[0x2d5] = stages; + rejected(queue); + } + for (const auto raster : {8u, 0x208u, 0x48u}) { + auto queue = pointSizeState(); + queue.context[0x205] = raster; + rejected(queue); + } + for (const auto flag : {1u << 17u, 1u << 20u}) { + auto queue = pointSizeState(); + queue.context[0x207] = 0x01210000 | flag; + rejected(queue); + } + for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) { + auto queue = pointSizeState(); + queue.context[0x207] |= distances; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances"); + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state"); + } + { + auto queue = pointSizeState(); + queue.context[0x207] |= 0x004001ffu; + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + } + for (const auto offset : {0x2d5u, 0x205u}) { + auto queue = pointSizeState(); + queue.context.erase(offset); + rejected(queue); + } + auto queue = pointSizeState(); + queue.userConfig.erase(0x242); + rejected(queue); +} + void hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { @@ -1380,6 +1464,17 @@ void DepthStencilTests() { queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u); const auto layered = AgcDriver::Graphics::DecodeState(queue); Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export"); + { + auto pointSize = queue; + pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck"); + pointSize.context[0x207] |= 1u << 16u; + const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false); + Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection); + const auto decoded = AgcDriver::Graphics::DecodeState(pointSize); + Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing"); + Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state"); + } queue.context[0x31b] = 1u | (8u << 13u); Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice"); queue.context[0x207] = 0; @@ -5534,6 +5629,7 @@ int main(int argc, char** argv) { framebufferLayerTests(); ShaderStageTests(); NggVertexWorkgroupTests(); + inactiveVertexPointSizeTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp new file mode 100644 index 0000000000..9cc96a457c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto draw = [&](std::array& memory, std::uint32_t exports) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x69, {0x207, exports}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 0x01200000); + draw(EnabledMemory, 0x01210000); + return commands; +} + +void InactivePointSizeDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without USE_VTX_POINT_SIZE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with USE_VTX_POINT_SIZE on a triangle draw and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + InactivePointSizeDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("USE_VTX_POINT_SIZE on a triangle draw drew the same pixels as the state without it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a8978eb6d3..05bf9cb7d5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,6 +2,7 @@ ### Unknown function info +- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured From aebc3f1480de68140bdc6c9bc1e96a16186d5340 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 08:59:11 +0100 Subject: [PATCH 075/353] feat(libc): export tzname ES-DE in RetroArch (PPSA99169) imports tzname from libSceLibcInternal and fails to load without it; ICU's uprv_tzname reads it after localtime. It points at the host's zone names and tzset refreshes it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Time.cpp | 20 +++++++++++++++----- core/libs/tests/GuestTimeConversion.cpp | 11 +++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 37f0ada0bf..4550a24038 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -402,10 +402,22 @@ std::size_t formatGuestTime(char* destination, std::size_t size, const char* for *buffer.cursor = '\0'; return static_cast(buffer.cursor - destination); } + +char** HostZoneNames() { +#ifdef _WIN32 + _tzset(); + return _tzname; +#else + ::tzset(); + return ::tzname; +#endif +} } extern "C" { +char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; + GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result); GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); @@ -413,11 +425,9 @@ std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const ch int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { -#ifdef _WIN32 - _tzset(); -#else - ::tzset(); -#endif + char** names = HostZoneNames(); + tzname_nid_postfix[0] = names[0]; + tzname_nid_postfix[1] = names[1]; } int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer) { diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 624508e4d6..b11b4a0f2f 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -23,6 +23,7 @@ char* APS5_VABI asctime_nid_postfix(const GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); void APS5_VABI tzset_nid_postfix(void); +extern char* tzname_nid_postfix[2]; char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); @@ -388,4 +389,14 @@ int main() { GuestTm shifted{}; Require(localtime_s_nid_postfix(&epoch, &shifted) == &shifted && shifted.tm_hour == 5 && shifted.tm_gmtoff == 18000, "tzset did not pick up the new TZ"); + Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr, + "tzname does not name the zone tzset set"); +#ifdef _WIN32 + _putenv_s("TZ", "EST5EDT"); +#else + setenv("TZ", "EST5EDT", 1); +#endif + tzset_nid_postfix(); + Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], "EDT") == 0, + "tzname does not name the standard and daylight zones"); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 08ed06628c..41626ae064 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -55,6 +55,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. +- [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 - [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. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) From 80d8bd06beb2d1900d8b630bf9d29f3fa204d88c Mon Sep 17 00:00:00 2001 From: XairRrR Date: Mon, 12 Oct 2026 19:39:42 +0200 Subject: [PATCH 076/353] fix(agc): share a depth plane's contents across its stencil pairings A depth surface is keyed by its depth and stencil addresses, so a depth plane bound with different stencil planes became separate Vulkan images. Depth written through one pairing and HTILE fast clears consumed by one were invisible to the others, and later draws tested against stale depth. Track which surface owns the current contents of each depth and stencil plane address. Binding a plane through another surface of the same format, sample count and extent copies that aspect over, or inherits a pending clear. Sampling a plane address reads the surface that bound it last. --- .../Graphics/src/DepthSurface.cpp | 151 +++++++++++++----- .../tests/DepthSurfaceReuse.cpp | 34 +++- 2 files changed, 146 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9337c6ef16..e729489352 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -143,6 +143,24 @@ public: return texture; } + void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { + pendingClear &= ~static_cast(aspect); + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + constexpr VkAccessFlags all = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, all, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkImageCopy region{}; + region.srcSubresource = {static_cast(aspect), 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, source.image, VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, all); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } + bool Holds(const Texture* texture) const { return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; }); } @@ -453,6 +471,64 @@ std::vector>& surfaces() { return *list; } +struct PlaneOwner { + DepthSurface* surface = nullptr; + std::uint64_t serial = 0; +}; + +using PlaneOwners = std::map, PlaneOwner>; + +std::uint64_t planeBinds = 0; + +PlaneOwners& depthOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +PlaneOwners& stencilOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address, VkImageAspectFlagBits aspect) { + auto& entry = owners[{surface.context.device, address}]; + auto* owner = entry.surface; + if (owner != nullptr && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) { + if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect; + else surface.CopyAspectFrom(*owner, aspect); + } + entry = {&surface, ++planeBinds}; +} + +void takePlanes(DepthSurface& surface) { + takePlane(surface, depthOwners(), surface.target.address, VK_IMAGE_ASPECT_DEPTH_BIT); + if (surface.target.stencilAddress != 0) takePlane(surface, stencilOwners(), surface.target.stencilAddress, VK_IMAGE_ASPECT_STENCIL_BIT); +} + +DepthSurface* depthSlice(VkDevice device, std::uint64_t address, const DepthTarget& base) { + if (const auto found = depthOwners().find({device, address}); found != depthOwners().end()) { + const auto& target = found->second.surface->target; + if (target.extent.width == base.extent.width && target.extent.height == base.extent.height && target.format == base.format) return found->second.surface; + } + const auto& list = surfaces(); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); + return slice == list.rend() ? nullptr : slice->get(); +} + +DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { + const auto depth = depthOwners().find({device, address}); + const auto stencil = stencilOwners().find({device, address}); + const bool hasDepth = depth != depthOwners().end(); + const bool hasStencil = stencil != stencilOwners().end(); + if (hasDepth && (!hasStencil || depth->second.serial > stencil->second.serial)) return depth->second.surface; + if (hasStencil) return stencil->second.surface; + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); + }); + return found == list.rend() ? nullptr : found->get(); +} + std::set& clearedHtiles() { static auto* cleared = new std::set(); return *cleared; @@ -509,21 +585,27 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; - for (const auto& surface : surfaces()) { - if (surface->context.device != context.device || !sameSurface(surface->target, target)) continue; - surface->clearDepth = target.clearDepth; - surface->clearStencil = target.clearStencil; - if (cleared) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; - surface->ApplyFastClear(); - surface->written = ++depthWrites; - drawnDepth = surface.get(); - return surface->view; + DepthSurface* surface = nullptr; + for (const auto& candidate : surfaces()) { + if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { + surface = candidate.get(); + break; + } } - surfaces().push_back(std::make_unique(context, target)); - surfaces().back()->clearDepth = target.clearDepth; - surfaces().back()->clearStencil = target.clearStencil; - drawnDepth = surfaces().back().get(); - return surfaces().back()->view; + const bool created = surface == nullptr; + if (created) { + surfaces().push_back(std::make_unique(context, target)); + surface = surfaces().back().get(); + } + const bool inherits = depthOwners().contains({context.device, target.address}); + surface->clearDepth = target.clearDepth; + surface->clearStencil = target.clearStencil; + takePlanes(*surface); + if (cleared && (!created || inherits)) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->ApplyFastClear(); + if (!created || inherits) surface->written = ++depthWrites; + drawnDepth = surface; + return surface->view; } std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { @@ -573,6 +655,8 @@ void NoteHtileDepthClear(std::uint64_t htileAddress) { void ClearDepthSurfaces(VkDevice device) { std::lock_guard lock(surfacesMutex()); drawnDepth = nullptr; + std::erase_if(depthOwners(), [&](const auto& entry) { return entry.first.first == device; }); + std::erase_if(stencilOwners(), [&](const auto& entry) { return entry.first.first == device; }); std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); } @@ -587,12 +671,9 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spancontext.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); - }); - if (found == list.rend()) return nullptr; - const auto& base = (*found)->target; + auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return nullptr; + const auto& base = found->target; if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr; const bool stencil = base.stencilAddress != 0 && resource.baseAddress == base.stencilAddress; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; @@ -601,11 +682,11 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanApplyFastClear(); + found->ApplyFastClear(); const bool cube = resource.dimension == TextureDimension::kCube; const bool layered = cube || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray != 0); - if (stencil || (!layered && !integer)) return (*found)->Sampled(words, resource, components); - Require((*found)->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); + if (stencil || (!layered && !integer)) return found->Sampled(words, resource, components); + Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { @@ -616,10 +697,10 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span slices; for (std::uint32_t layer = 0; layer < layers; ++layer) { const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); - if (slice != list.rend()) (*slice)->ApplyFastClear(); - if (slice == list.rend()) slices.push_back({}); - else slices.push_back({(*slice)->image, slice->get() == drawnDepth ? 0u : (*slice)->written}); + auto* slice = depthSlice(context.device, address, base); + if (slice != nullptr) slice->ApplyFastClear(); + if (slice == nullptr) slices.push_back({}); + else slices.push_back({slice->image, slice == drawnDepth ? 0u : slice->written}); } const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT; auto& copy = planeCopies()[{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}]; @@ -633,11 +714,8 @@ bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resou if (resource.dimension != TextureDimension::k2D || resource.baseArray != 0) return false; const auto address = resource.baseAddress; std::lock_guard lock(surfacesMutex()); - const auto& list = surfaces(); - const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); - }); - return found != list.rend() && (*found)->Holds(texture); + const auto* found = planeSurface(context.device, address); + return found != nullptr && found->Holds(texture); } VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile) { @@ -668,12 +746,9 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { std::lock_guard lock(surfacesMutex()); - const auto& list = surfaces(); - const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); - }); - if (found == list.rend()) return false; - const auto& target = (*found)->target; + const auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return false; + const auto& target = found->target; if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index ff2d18e119..deec634f74 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include namespace { @@ -35,10 +37,13 @@ THandle newHandle() { } VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; +std::vector createdImages; +std::vector> imageCopies; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; *image = newHandle(); + createdImages.push_back(*image); return VK_SUCCESS; } @@ -97,6 +102,9 @@ VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCal VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); +} VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { *properties = {}; @@ -133,7 +141,8 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkDestroyImage", reinterpret_cast(destroyImage)}, {"vkFreeMemory", reinterpret_cast(freeMemory)}, {"vkCmdPipelineBarrier", reinterpret_cast(cmdPipelineBarrier)}, - {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)} + {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)}, + {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -276,5 +285,28 @@ void RunDepthSurfaceReuseTests() { Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + constexpr std::uint64_t shared = 0x70000000; + constexpr std::uint64_t stencilA = 0x71000000; + constexpr std::uint64_t stencilB = 0x72000000; + constexpr std::uint64_t otherDepth = 0x73000000; + const auto pairing = [&](std::uint64_t depth, std::uint64_t stencil) { + const auto before = createdImages.size(); + DepthSurfaceView(context, {depth, stencil, {256, 256}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + return createdImages.size() != before ? createdImages[before] : VK_NULL_HANDLE; + }; + const auto withA = pairing(shared, stencilA); + imageCopies.clear(); + const auto withB = pairing(shared, stencilB); + Require(withA != VK_NULL_HANDLE && withB != VK_NULL_HANDLE && withA != withB, "each depth/stencil pairing must have its own image"); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, withB, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "a depth plane paired with another stencil plane must carry its depth over"); + imageCopies.clear(); + pairing(shared, stencilB); + Require(imageCopies.empty(), "drawing again with the same pairing must not copy"); + pairing(shared, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withB, withA, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "returning to the first pairing must carry the depth written through the second"); + imageCopies.clear(); + const auto otherWithA = pairing(otherDepth, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + ClearDepthSurfaces(context.device); } From 4b7a357bd222980911fda3491cac79b0c31a9bfb Mon Sep 17 00:00:00 2001 From: bubblegg <86165363+bubblegg@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:15:41 +0000 Subject: [PATCH 077/353] fix(savedata): report a save's size as the mount that created it gave Every native save reported SAVE_DATA_BLOCKS_MAX blocks, all of them free, in the sceSaveDataDirNameSearch infos and in sceSaveDataGetMountInfo, whatever the title created it with and whatever its files held. A save's size is now the blocks of the sceSaveDataMount3 call that created it, kept beside the save; a save without a kept size takes that of its next mount that names one, and otherwise keeps SAVE_DATA_BLOCKS_MAX. Free blocks are what the save's files leave of it, each file rounded up to 32 KiB blocks, and the blocks and free-blocks sort keys use both. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 65 +++++++++++++++++-- core/libs/tests/GuestSaveDataSearch.cpp | 65 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 08cc546d3d..269106de89 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -219,6 +219,10 @@ int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { return g_transaction_counter.fetch_add(1); } +static std::string blocks_path(const std::string& real_path) { + return real_path + ".blocks"; +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { throw std::runtime_error("sceSaveDataDelete: null argument"); @@ -254,6 +258,7 @@ static int deleteSave(const SaveDataDelete* del) { } std::error_code ec; std::filesystem::remove(param_path(path), ec); + std::filesystem::remove(blocks_path(path), ec); return SAVE_DATA_OK; } @@ -268,6 +273,45 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; + +static std::uint64_t save_blocks(const std::string& real_path) { + std::ifstream file(blocks_path(real_path)); + std::uint64_t blocks = 0; + if (!(file >> blocks) || blocks == 0) { + return SAVE_DATA_BLOCKS_MAX; + } + return std::min(blocks, SAVE_DATA_BLOCKS_MAX); +} + +static void keep_save_blocks(const std::string& real_path, std::uint64_t blocks) { + std::ofstream file(blocks_path(real_path), std::ios::trunc); + file << std::min(blocks, SAVE_DATA_BLOCKS_MAX) << '\n'; + if (!file) { + throw std::runtime_error("sceSaveDataMount3: the save's size could not be kept"); + } +} + +static std::uint64_t used_blocks(const std::string& real_path) { + std::uint64_t blocks = 0; + std::error_code error; + for (auto it = std::filesystem::recursive_directory_iterator(real_path, error); !error && it != std::filesystem::recursive_directory_iterator(); it.increment(error)) { + if (!it->is_regular_file(error)) { + continue; + } + const auto bytes = it->file_size(error); + if (!error) { + blocks += (bytes + SAVE_DATA_BLOCK_BYTES - 1) / SAVE_DATA_BLOCK_BYTES; + } + } + return blocks; +} + +static SaveDataSearchInfo search_info(const std::string& real_path) { + const auto blocks = save_blocks(real_path); + return SaveDataSearchInfo{blocks, blocks - std::min(blocks, used_blocks(real_path)), {}}; +} + static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); @@ -287,6 +331,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS std::string name; std::string path; SaveDataParam param; + SaveDataSearchInfo info; }; const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; std::vector matches; @@ -298,7 +343,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - Match match{name, entry.path().string(), {}}; + Match match{name, entry.path().string(), {}, search_info(entry.path().string())}; if (sortByParam) { match.param = load_param(match.path); } @@ -309,6 +354,10 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); } else if (key == SAVE_DATA_SORT_KEY_MTIME) { std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } else if (key == SAVE_DATA_SORT_KEY_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.blocks < b.info.blocks; }); + } else if (key == SAVE_DATA_SORT_KEY_FREE_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.free_blocks < b.info.free_blocks; }); } if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { std::reverse(matches.begin(), matches.end()); @@ -320,7 +369,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } if (result->infos != nullptr) { - result->infos[i] = SaveDataSearchInfo{SAVE_DATA_BLOCKS_MAX, SAVE_DATA_BLOCKS_MAX, {}}; + result->infos[i] = matches[i].info; } } result->hit_num = static_cast(matches.size()); @@ -353,12 +402,14 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } std::lock_guard lock(g_slots_mutex); - if (find_slot_by_mount_point(mount_point->data) == -1) { + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } std::memset(info, 0, sizeof(*info)); - info->blocks = SAVE_DATA_BLOCKS_MAX; - info->free_blocks = SAVE_DATA_BLOCKS_MAX; + const auto search = search_info(g_slots[slot].real_path); + info->blocks = search.blocks; + info->free_blocks = search.free_blocks; return SAVE_DATA_OK; } @@ -512,6 +563,10 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); mount_result->required_blocks = 0; mount_result->mount_status = (create || (create2 && !exists)) ? 1u : 0u; + std::error_code sized; + if (mount->blocks != 0 && (create || (create2 && !exists) || !std::filesystem::exists(blocks_path(real_path), sized))) { + keep_save_blocks(real_path, mount->blocks); + } return SAVE_DATA_OK; } diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 53d0cc5066..066dbd7dfc 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDa int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); } static constexpr int SaveDataErrorParameter = -2137063424; @@ -171,6 +173,68 @@ static void SearchAndMount() { } } +static void CreateSized(const char* name, std::uint64_t blocks, std::size_t bytes) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.blocks = blocks; + mount.mount_mode = 2 | 4; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "creating a sized save failed"); + if (bytes != 0) { + std::ofstream file(std::filesystem::path("_sd") / name / "data", std::ios::binary); + file << std::string(bytes, 'x'); + } + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + +static std::vector SizeSearch(std::uint32_t key, std::vector& found) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SIZE_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + std::array names{}; + std::array infos{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 2; + result.infos = infos.data(); + Require(sceSaveDataDirNameSearch(&cond, &result) == 0 && result.set_num == 2, "the sized search must return both saves"); + found.clear(); + for (const auto& name : names) found.emplace_back(name.data); + return {infos.begin(), infos.end()}; +} + +static void SaveSize() { + CreateSized("SIZE_A", 200, 0); + CreateSized("SIZE_B", 96, 40000); + std::vector found; + auto infos = SizeSearch(0, found); + Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); + Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); + SizeSearch(2, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); + infos = SizeSearch(5, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 1; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); + SaveDataMountInfo info{}; + Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); + Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / @@ -182,6 +246,7 @@ int main() { SearchAndMount(); SortByKey(); TrailingWildcards(); + SaveSize(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 45da9c14c8..c8a63945c5 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -7,7 +7,7 @@ - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown -- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`) report `SAVE_DATA_BLOCKS_MAX` for both. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown ### Functional From 44767a7b97b918d215f5dd700231bc1055943611 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:22:35 +0200 Subject: [PATCH 078/353] perf(libc): size each write-watch query to the region it covers WindowsMappings::Collect handed GetWriteWatch the whole remaining output capacity for every private region it walked. The call's cost follows that capacity, not the size of the range: on Windows 11 (26200) a query of one 16 KiB region took 0.35 us with room for 4 entries and 6.5 us with room for the 65536 the write tracker passes. Flexible memory is committed in 16 KiB allocations, one region each, so a resetting walk over 33 MiB of it took 14.7 ms, 5.8 times a memcmp of the same bytes. Each query now gets room for the pages its region spans, which is the most it can report, so every walk returns the same pages; the 33 MiB walk takes 1.23 ms. guest_memory checks that a walk over eight 16 KiB allocations reports every stored page once, in order, with room for 32, 5 and 1 pages. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libc/include/WindowsMappings.hpp | 5 ++- core/libs/tests/GuestMemory.cpp | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index d93c344e62..2bd449f022 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -346,8 +346,9 @@ public: const auto memory = query(cursor); if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); - ULONG_PTR available = capacity - *count; - if (available == 0) return true; + if (capacity == *count) return true; + const auto touched = (((stop + 4095) & ~static_cast(4095)) - (cursor & ~static_cast(4095))) / 4096; + ULONG_PTR available = std::min(capacity - *count, touched); DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); *count += available; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 9a12142d84..5c0916db99 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1492,6 +1492,43 @@ static void CheckFailedCollectKeepsWrites() { #endif } +static void CheckSplitPrivateCollect() { +#ifdef _WIN32 + constexpr std::size_t granule = 0x4000; + constexpr std::size_t regions = 8; + constexpr std::size_t bytes = granule * regions; + constexpr std::size_t pages = bytes / 4096; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, granule)); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, granule); + const auto address = reinterpret_cast(block); + const auto collect = [&](std::size_t capacity) { + std::vector found; + std::array buffer{}; + for (auto cursor = address; cursor < address + bytes;) { + std::size_t count = capacity; + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, address + bytes - cursor, buffer.data(), &count, true)); + for (std::size_t i = 0; i < count; ++i) found.push_back(reinterpret_cast(buffer[i])); + if (count < capacity) break; + cursor = found.back() + 4096; + } + return found; + }; + collect(pages); + for (const std::size_t capacity : {pages, std::size_t{5}, std::size_t{1}}) { + for (std::size_t page = 0; page < pages; ++page) static_cast(block)[page * 4096 + 7] = static_cast(capacity + page); + const auto found = collect(capacity); + Require(found.size() == pages); + for (std::size_t page = 0; page < pages; ++page) Require(found[page] == address + page * 4096); + Require(collect(pages).empty()); + } + static_cast(block)[granule * 3 + 4096 + 1] = 9; + const auto one = collect(pages); + Require(one.size() == 1 && one[0] == address + granule * 3 + 4096); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#endif +} + static void CheckPinnedSharedPages() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -2156,6 +2193,7 @@ int main() { CheckFixedMappingReplacesPartialOverlap(); CheckFixedMappingOverHoleInsideAllocation(); CheckDirectMemoryGpuProtBits(); + CheckSplitPrivateCollect(); CheckGpuAccessFollowsProtection(); CheckGpuMapsAreObserved(); CheckClearingTheObserverWaitsForItsCalls(); From 230b5c26833bfe246ee5aa716d2fd45228b2a866 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:00:43 +0200 Subject: [PATCH 079/353] perf(agc-driver): join the imports of adjacent large ranges when a buffer is bound across them 8 MiB window, so a range larger than the window is always imported alone. A buffer bound across two such ranges then has no import and is copied whole on every use: PPSA25344 binds a 368 MiB buffer across a 320.75 MiB and a 47.3 MiB range and copies it on almost every frame. When UploadFinish meets such a region, it imports the run of the ranges the region crosses, provided every one of them is larger than the window, and retires their own imports. It does this before any region of the build confirms an import, so the retire moves the epoch ahead of every pointer the build would use. A range that large is registered whole, not grown in 64 KiB pieces like a heap, so the joined import is not re-imported as a heap grows; it stands in refreshImports while exactly its ranges stand. A later join that reaches a joined import takes it in, so two joins that share a range never retire each other in turn. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 85 ++++++++++++++++--- .../libSceAgcDriver/tests/BdaResources.cpp | 76 +++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 152 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 01c754b506..2a74aebf18 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -33,6 +33,7 @@ struct HostImport { std::uint64_t serial = 0; bool unwatched = false; bool dmaBuf = false; + bool joined = false; std::shared_ptr chunk; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index ec5d3a4f55..15d1cbfd1a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -161,6 +161,7 @@ struct HostImports { std::map>> parked; std::uint64_t liveBytes = 0; std::map failed; + std::map failedJoins; std::set unaliasedSpans; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; @@ -210,12 +211,13 @@ struct RetiredImport { const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); void retireImport(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, bool hold = true) { // Results shadowed for the import reach its (old) buffer first where the memory is still a // readable registered range (the import retires because its registration vanished or changed // size); the holder below outlives the batch that copies them. - RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); }); + RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); }); ++state.epoch; state.liveBytes -= it->second.bytes; auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it)); @@ -630,6 +632,27 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } +ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto window = importWindow.load(std::memory_order_relaxed); + auto last = leasedIndexContaining(lease, begin); + if (window == 0 || last == lease.size()) return {}; + const auto first = last; + for (;;) { + if (lease[last]->bytes <= window) return {}; + if (lease[last]->address + lease[last]->bytes >= end) break; + if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; + ++last; + } + if (first == last) return {}; + ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; + run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); + return run; +} + +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + return !joinedRun(lease, begin, end).ranges.empty(); +} + template bool sameRun(const Entry& entry, const ImportRun& run) { if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false; @@ -652,20 +675,42 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { } bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { - return sameRun(entry, importRun(lease, entry.base)); + return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base)); } 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); const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) { if (!importsStale(context, state)) { if (const auto* current = findImport(state, address, address + 1)) return current; + } else if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + return current; } auto run = importRun(lease, address); if (run.ranges.empty()) return nullptr; + return importRunAt(context, state, std::move(run), false, address, lease); +} + +const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { + if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; + if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; + auto run = joinedRun(lease, begin, end); + if (run.ranges.empty()) return nullptr; + if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr; + return importRunAt(context, state, std::move(run), true, begin, lease); +} + +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease) { const auto base = run.base; const auto bytes = run.bytes; + auto& failures = joined ? state.failedJoins : state.failed; + const auto alignment = context.hostImportAlignment; + if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) { + 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()) { if (sameRun(found->second, run)) return &found->second; retireImport(context.device, state, found, lease); @@ -676,11 +721,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s it = next; } if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease); - const auto alignment = context.hostImportAlignment; if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; - if (const auto failed = state.failed.find(base); failed != state.failed.end()) { + if (const auto failed = failures.find(base); failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; - state.failed.erase(failed); + failures.erase(failed); } // Pinned imports count against the driver's system memory budget; past it ordinary host // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the @@ -713,7 +757,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s for (std::uint64_t cursor = base; cursor < base + bytes;) { MEMORY_BASIC_INFORMATION info{}; if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } const auto protection = info.Protect & 0xffu; @@ -725,7 +769,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } if (!writable && readOnly) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } if (!writable) { @@ -738,13 +782,17 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); } catch (const std::runtime_error&) { if (entry.ranges.size() == 1) throw; + if (joined) { + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + return nullptr; + } state.unaliasedSpans.insert(base); return importAllocation(context, state, address, lease); } } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } #endif @@ -775,7 +823,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); #ifdef _WIN32 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; @@ -805,7 +853,8 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s importedBytes += bytes; static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; state.liveBytes += bytes; - if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0); + if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); + entry.joined = joined; return &state.imports.emplace(base, entry).first->second; } @@ -839,6 +888,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc } state.liveBytes = 0; state.failed.clear(); + state.failedJoins.clear(); state.device = context.device; state.destroyBuffer = context.Function("vkDestroyBuffer"); state.freeMemory = context.Function("vkFreeMemory"); @@ -864,6 +914,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc it = next; } for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it); + 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) { @@ -1812,6 +1863,7 @@ void ClearHostImports(VkDevice device) { state.imports.clear(); state.liveBytes = 0; state.failed.clear(); + state.failedJoins.clear(); state.device = VK_NULL_HANDLE; state.refreshedGeneration = 0; ++state.epoch; @@ -2011,7 +2063,7 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - if (!runCovers(lease, address, address + bytes)) return nullptr; + if (!runCovers(lease, address, address + bytes)) return findImport(state, address, address + bytes); const auto* entry = importAllocation(context, state, address, lease); return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr; } @@ -2937,6 +2989,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) { ++Mirrors().subranges; } bool importsRefreshed = false; + if (context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) refreshImports(context, state, importRanges()); + importsRefreshed = true; + for (const auto& region : regions) { + if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue; + if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue; + static_cast(importJoined(context, state, region.begin, region.end, importRanges())); + } + } if (space != nullptr && context.hostImportAlignment != 0) { // The space's regions were confirmed against the imports when it was built; one check that // none was retired since (a mirror refresh or a flush between the stages can reconcile diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index cca74f7895..d5ce3550e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -207,6 +207,81 @@ void importWindowTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +void importJoinTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t large = 3 * piece; + void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + std::memset(guest, 0x11, 3 * large); + const auto first = reinterpret_cast(block); + const auto second = first + large; + const auto third = second + large; + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto bind = [&](std::uintptr_t address) { + GuestBufferMemory plain(importing); + plain.AddReadable(address, 32); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(address, 32, adjustment); + const auto copied = plain.CopiedBytes(); + plain.WriteBack(); + return std::pair{view.buffer, copied}; + }; + change(guest, large, true); + change(guest + large, large, true); + const auto* low = HostImportFor(importing, first, 16); + Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone"); + const auto* high = HostImportFor(importing, second, 16); + Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone"); + Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound"); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 16, 32); + leased.Upload(true); + const auto* joined = HostImportFor(importing, second - 16, 32); + Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one"); + std::uint32_t adjustment = 0; + const auto crossing = leased.Descriptor(second - 16, 32, adjustment); + Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place"); + leased.WriteBack(); + } + const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer; + const auto [again, copied] = bind(second - 16); + Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import"); + auto* heap = guest + 3 * large + 2 * piece; + change(heap, piece, true); + change(heap + piece, piece, true); + Require(bind(reinterpret_cast(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined"); + const auto* kept = HostImportFor(importing, first, 16); + Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); + change(guest + 2 * large, large, true); + Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them"); + const auto [grownBuffer, grownCopied] = bind(third - 16); + const auto* grown = HostImportFor(importing, third - 16, 32); + Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import"); + Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other"); + change(heap, piece, false); + change(heap + piece, piece, false); + change(guest + large, large, false); + const auto* alone = HostImportFor(importing, first, 16); + Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges"); + change(guest, large, false); + change(guest + 2 * large, large, false); + Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges"); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1203,6 +1278,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importSpanTests(context); failedImportRemapTests(context, access); importWindowTests(context); + importJoinTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 096a40be4b..c67c41bf63 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,7 +69,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, codes other than 0 and 0xF, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. When the clear runs as a compute pass (a resident target whose CMASK is host-imported), an invalid code throws at the next fast-clear materialization, not at the one that read it - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. -- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. +- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. From d34b78890d43c636227981090a1aaf4501e283ae Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Tue, 13 Oct 2026 17:39:38 +0530 Subject: [PATCH 080/353] fix(savedata): return no saves for a search with another title's ID Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 4 ++ core/libs/tests/GuestSaveDataSearch.cpp | 38 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 43 insertions(+) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 08cc546d3d..4fa38b63d7 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -278,6 +279,9 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { return SAVE_DATA_ERROR_PARAMETER; } + if (cond->title_id != nullptr && std::strncmp(cond->title_id->data, GetAppTitleId_nid_postfix().value, sizeof(cond->title_id->data)) != 0) { + return SAVE_DATA_OK; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 53d0cc5066..fb8e61c70d 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -123,6 +124,42 @@ static void TrailingWildcards() { Require(SearchPattern("TRAIL_%") == Names{"TRAIL1"}, "_ must still match exactly one character"); } +static std::uint32_t SearchTitle(const char* titleId, bool second) { + SceSaveDataTitleId title{}; + std::snprintf(title.data, sizeof(title.data), "%s", titleId != nullptr ? titleId : ""); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "OTHER_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.title_id = titleId != nullptr ? &title : nullptr; + cond.dir_name = &pattern; + std::array names{}; + names[0].data[0] = '!'; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 1; + result.hit_num = 7; + result.set_num = 7; + Require((second ? sceSaveDataDirNameSearch2(&cond, &result) : sceSaveDataDirNameSearch(&cond, &result)) == 0, "title search failed"); + Require(result.set_num == std::min(result.hit_num, 1), "title search must set the names it found"); + Require(result.set_num == 1 ? std::string(names[0].data) == "OTHER_A" : names[0].data[0] == '!', "title search returned the wrong name"); + return result.hit_num; +} + +static void OtherTitle() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs("{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}", param); + std::fclose(param); + std::filesystem::create_directories(std::filesystem::path("_sd") / "OTHER_A"); + for (const bool second : {false, true}) { + Require(SearchTitle(nullptr, second) == 1, "a search without a title id must find the running title's save"); + Require(SearchTitle("PPSA00000", second) == 1, "a search for the running title must find its save"); + Require(SearchTitle("PPSA17221", second) == 0, "a search for another title must find none of the running title's saves"); + } +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -181,6 +218,7 @@ int main() { try { SearchAndMount(); SortByKey(); + OtherTitle(); TrailingWildcards(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 45da9c14c8..f83d430f9e 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -12,5 +12,6 @@ ### Functional +- [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. From bc014a887cc5cd78b1aa4d2597d1cdb103e2ebe4 Mon Sep 17 00:00:00 2001 From: Aarav <211792885+AaravMehta-07@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:06:18 +0530 Subject: [PATCH 081/353] fix(relinker): reject load file sizes beyond memory size --- core/relinker/relinker/src/parsing/ElfReader.cpp | 3 +++ .../relinker/tests/test_linux_load_alignment.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index a214a56f8d..81c62ebd8e 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -130,6 +130,9 @@ std::vector ElfReader::ReadProgramHeaders() const { ph.MemorySize = _readU64At(offset + 0x28); ph.Alignment = _readU64At(offset + 0x30); + if (ph.Type == PT_LOAD && ph.FileSize > ph.MemorySize) + throw RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); + headers.push_back(ph); offset += header.ProgramHeaderEntrySize; } diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index f3b0c356b3..51008e9392 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -54,6 +54,16 @@ def main(): if alignment != 0x4000 or first[3] % alignment != 0 or first[7] != alignment: raise AssertionError(("first PT_LOAD is not aligned to the largest segment alignment", segments)) assert " |-- output.elf\n |-- libs/\n |-- *.prx (system libraries)\n |-- app0/" in result.stdout, result.stdout + + malformed = fixture() + struct.pack_into(" Date: Sun, 11 Oct 2026 17:53:11 +0200 Subject: [PATCH 082/353] feat(libScePosixForWebKit): implement getservbyname Implement getservbyname (H1bArJPagV0) in libScePosixForWebKit. Cloud Play (PPSA99600) imports it from this library only (registry index 408, call site 0x1042124); once fnmatch (#3545) is in, its predicted load stop is: FAIL: unresolved ELF import H1bArJPagV0 from libScePosixForWebKit.prx The ps5-payload-dev SDK stubs (f5d40f02, sce_stubs/) declare getservbyname in libScePosixForWebKit.c and not in libkernel.c or libSceLibcInternal.c. The call site is the service-name branch of a getaddrinfo inside the title: a non-numeric service is looked up with protocol "tcp", "udp" or "sctp" and the port is read big-endian at offset 16 of the result, which fixes the servent layout used here (name, aliases, 32-bit port, protocol at 0, 8, 16, 24). The guest has no /etc/services and the console's file is unknown, so the function searches 37 lines of FreeBSD 11.4 etc/services (8c639945): ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps and pop3s with their aliases and protocols, in file order. None of their names or aliases appears on an earlier line of the full file, so these lookups answer as the full file would. The search follows files_servent in lib/libc/net/getservent.c (8c639945): exact, case-sensitive match on the name or an alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage like getserv's NSS TLS state. Any other name returns null without touching errno, as for a name the file lacks. The repository's getaddrinfo still resolves service names through the host, so there was no table to share; this is recorded in TechnicalDebt.md. Co-Authored-By: Claude Fable 5.1 --- .../prx/libScePosixForWebKit/CMakeLists.txt | 9 +++ .../libScePosixForWebKit/GuestResolver.hpp | 11 ++++ .../prx/libScePosixForWebKit/Resolver.cpp | 61 ++++++++++++++++++ core/libs/tests/GuestServiceDatabase.cpp | 62 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 1 + 5 files changed, 144 insertions(+) create mode 100644 core/libs/tests/GuestServiceDatabase.cpp diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index fddddc543d..036eb0577d 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -36,4 +36,13 @@ if(BUILD_TESTING) set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_service_database_tests ${LIBS_INCLUDE_DIR}/tests/GuestServiceDatabase.cpp) + target_include_directories(guest_service_database_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_service_database_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_service_database_tests) + add_test(NAME guest_service_database COMMAND guest_service_database_tests) + if(WIN32) + set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp index 7ed54e7160..5e6fc068cc 100644 --- a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp +++ b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp @@ -28,4 +28,15 @@ struct AddressInfo { static_assert(sizeof(AddressInfo) == 48); static_assert(offsetof(AddressInfo, canonicalName) == 24); static_assert(offsetof(AddressInfo, address) == 32); + +struct ServiceEntry { + char* name; + char** aliases; + std::int32_t port; + char* protocol; +}; +static_assert(sizeof(ServiceEntry) == 32); +static_assert(offsetof(ServiceEntry, aliases) == 8); +static_assert(offsetof(ServiceEntry, port) == 16); +static_assert(offsetof(ServiceEntry, protocol) == 24); } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 6441693f4b..9333e9c7e4 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -31,6 +31,41 @@ struct HostLookup { std::vector addresses; }; +struct ServiceRecord { + const char* name; + std::uint16_t port; + const char* protocol; + std::array aliases; +}; + +constexpr ServiceRecord Services[]{ + {"ftp-data", 20, "tcp", {}}, {"ftp-data", 20, "udp", {}}, {"ftp-data", 20, "sctp", {}}, + {"ftp", 21, "tcp", {}}, {"ftp", 21, "udp", {}}, {"ftp", 21, "sctp", {}}, + {"ssh", 22, "tcp", {}}, {"ssh", 22, "udp", {}}, {"ssh", 22, "sctp", {}}, + {"telnet", 23, "tcp", {}}, {"telnet", 23, "udp", {}}, + {"smtp", 25, "tcp", {"mail"}}, {"smtp", 25, "udp", {"mail"}}, + {"domain", 53, "tcp", {}}, {"domain", 53, "udp", {}}, + {"tftp", 69, "tcp", {}}, {"tftp", 69, "udp", {}}, + {"http", 80, "tcp", {"www", "www-http"}}, {"http", 80, "udp", {"www", "www-http"}}, + {"http", 80, "sctp", {"www", "www-http"}}, + {"pop3", 110, "tcp", {}}, {"pop3", 110, "udp", {}}, + {"ntp", 123, "tcp", {}}, {"ntp", 123, "udp", {}}, + {"imap", 143, "tcp", {"imap2", "imap4"}}, {"imap", 143, "udp", {"imap2", "imap4"}}, + {"https", 443, "tcp", {}}, {"https", 443, "udp", {}}, {"https", 443, "sctp", {}}, + {"rtsp", 554, "tcp", {}}, {"rtsp", 554, "udp", {}}, + {"submission", 587, "tcp", {}}, {"submission", 587, "udp", {}}, + {"imaps", 993, "tcp", {}}, {"imaps", 993, "udp", {}}, + {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, +}; + +struct ServiceLookup { + GuestResolver::ServiceEntry entry{}; + std::string name; + std::string protocol; + std::vector aliasNames; + std::vector aliases; +}; + bool NumericAddress(const char* name, std::array& address) { std::array parts{}; std::size_t count = 0; @@ -155,6 +190,32 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) } } +GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* name, const char* protocol) { + for (const auto& service : Services) { + bool named = std::strcmp(name, service.name) == 0; + for (const char* alias : service.aliases) + named = named || (alias && std::strcmp(name, alias) == 0); + if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; + try { + static thread_local ServiceLookup lookup; + lookup.name = service.name; + lookup.protocol = service.protocol; + lookup.aliasNames.clear(); + lookup.aliases.clear(); + for (const char* alias : service.aliases) + if (alias) lookup.aliasNames.emplace_back(alias); + for (auto& alias : lookup.aliasNames) lookup.aliases.push_back(alias.data()); + lookup.aliases.push_back(nullptr); + lookup.entry = {lookup.name.data(), lookup.aliases.data(), htons(service.port), lookup.protocol.data()}; + return &lookup.entry; + } catch (const std::bad_alloc&) { + errno = ENOMEM; + return nullptr; + } + } + return nullptr; +} + void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo* first) { while (first) { auto* next = first->next; diff --git a/core/libs/tests/GuestServiceDatabase.cpp b/core/libs/tests/GuestServiceDatabase.cpp new file mode 100644 index 0000000000..30caec9095 --- /dev/null +++ b/core/libs/tests/GuestServiceDatabase.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char*, const char*); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Service database check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool NetworkPort(const GuestResolver::ServiceEntry* entry, std::uint16_t port) { + unsigned char bytes[4]; + std::memcpy(bytes, &entry->port, sizeof(bytes)); + return bytes[0] == port >> 8 && bytes[1] == (port & 0xff) && bytes[2] == 0 && bytes[3] == 0; +} + +int main() { + auto* http = getservbyname_nid_postfix("http", "tcp"); + Require(http && std::strcmp(http->name, "http") == 0 && std::strcmp(http->protocol, "tcp") == 0); + Require(NetworkPort(http, 80)); + Require(http->aliases && std::strcmp(http->aliases[0], "www") == 0); + Require(std::strcmp(http->aliases[1], "www-http") == 0 && http->aliases[2] == nullptr); + + auto* https = getservbyname_nid_postfix("https", nullptr); + Require(https && std::strcmp(https->name, "https") == 0 && std::strcmp(https->protocol, "tcp") == 0); + Require(NetworkPort(https, 443) && https->aliases && https->aliases[0] == nullptr); + + auto* domain = getservbyname_nid_postfix("domain", "udp"); + Require(domain && std::strcmp(domain->name, "domain") == 0 && std::strcmp(domain->protocol, "udp") == 0); + Require(NetworkPort(domain, 53)); + + auto* www = getservbyname_nid_postfix("www", "sctp"); + Require(www && std::strcmp(www->name, "http") == 0 && std::strcmp(www->protocol, "sctp") == 0); + Require(NetworkPort(www, 80)); + + auto* mail = getservbyname_nid_postfix("mail", nullptr); + Require(mail && std::strcmp(mail->name, "smtp") == 0 && NetworkPort(mail, 25)); + + errno = 0; + Require(getservbyname_nid_postfix("no-such-service", nullptr) == nullptr && errno == 0); + Require(getservbyname_nid_postfix("domain", "sctp") == nullptr); + Require(getservbyname_nid_postfix("ntp", "TCP") == nullptr); + Require(getservbyname_nid_postfix("HTTP", "tcp") == nullptr); + + auto* ntp = getservbyname_nid_postfix("ntp", "udp"); + Require(ntp && NetworkPort(ntp, 123)); + std::thread([] { + auto* ssh = getservbyname_nid_postfix("ssh", "tcp"); + Require(ssh && std::strcmp(ssh->name, "ssh") == 0 && NetworkPort(ssh, 22)); + }).join(); + Require(std::strcmp(ntp->name, "ntp") == 0 && std::strcmp(ntp->protocol, "udp") == 0 && NetworkPort(ntp, 123)); + return 0; +} diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index cf5516d57a..c57da143e9 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -9,3 +9,4 @@ - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws +- [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database From 8b9529b99af3e7c87eef94d5268df92f6afe7743 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 13:16:38 +0200 Subject: [PATCH 083/353] fix(libScePosixForWebKit): keep the getservbyname result trivially destructible The per-thread servent storage held std::string and std::vector members, so getservbyname_nid_postfix registered a thread_local destructor through __cxa_thread_atexit. On Windows that destructor runs after emulated TLS freed the object, and prx_thread_local_destructors (#4029) rejects libScePosixForWebKit.prx for it. The result now lives in a thread_local struct of fixed-size char arrays and an alias pointer array, sized for the longest name (submission), alias (www-http), protocol (sctp) and alias count in the embedded table; static_asserts check the sizes against the table and that the struct is trivially destructible. The guest servent layout and the per-thread result FreeBSD's getservent.c keeps in its NSS TLS state are unchanged. Nothing is allocated, so the ENOMEM path is gone. Co-Authored-By: Claude Fable 5.1 --- .../prx/libScePosixForWebKit/Resolver.cpp | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 9333e9c7e4..0dd6901832 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -10,12 +10,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include #include #include #include #include +#include #include #include "GuestResolver.hpp" @@ -58,13 +60,38 @@ constexpr ServiceRecord Services[]{ {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, }; +constexpr std::size_t ServiceNameSize = 11; +constexpr std::size_t ServiceAliasSize = 9; +constexpr std::size_t ServiceProtocolSize = 5; +constexpr std::size_t ServiceAliasCount = std::tuple_size_v; + +template +constexpr std::size_t LongestServiceText(TSelect select) { + std::size_t longest = 0; + for (const auto& service : Services) + for (const char* text : select(service)) + if (text) longest = std::max(longest, std::char_traits::length(text)); + return longest; +} + +static_assert(ServiceNameSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.name}; }) + 1); +static_assert(ServiceAliasSize == LongestServiceText([](const ServiceRecord& record) { return record.aliases; }) + 1); +static_assert(ServiceProtocolSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.protocol}; }) + 1); + struct ServiceLookup { - GuestResolver::ServiceEntry entry{}; - std::string name; - std::string protocol; - std::vector aliasNames; - std::vector aliases; + GuestResolver::ServiceEntry entry; + std::array name; + std::array protocol; + std::array, ServiceAliasCount> aliasNames; + std::array aliases; }; +static_assert(std::is_trivially_destructible_v); + +template +char* CopyServiceText(std::array& target, const char* text) { + std::memcpy(target.data(), text, std::strlen(text) + 1); + return target.data(); +} bool NumericAddress(const char* name, std::array& address) { std::array parts{}; @@ -196,22 +223,17 @@ GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* nam for (const char* alias : service.aliases) named = named || (alias && std::strcmp(name, alias) == 0); if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; - try { - static thread_local ServiceLookup lookup; - lookup.name = service.name; - lookup.protocol = service.protocol; - lookup.aliasNames.clear(); - lookup.aliases.clear(); - for (const char* alias : service.aliases) - if (alias) lookup.aliasNames.emplace_back(alias); - for (auto& alias : lookup.aliasNames) lookup.aliases.push_back(alias.data()); - lookup.aliases.push_back(nullptr); - lookup.entry = {lookup.name.data(), lookup.aliases.data(), htons(service.port), lookup.protocol.data()}; - return &lookup.entry; - } catch (const std::bad_alloc&) { - errno = ENOMEM; - return nullptr; + static thread_local ServiceLookup lookup; + std::size_t count = 0; + for (const char* alias : service.aliases) { + if (!alias) continue; + lookup.aliases[count] = CopyServiceText(lookup.aliasNames[count], alias); + ++count; } + lookup.aliases[count] = nullptr; + lookup.entry = {CopyServiceText(lookup.name, service.name), lookup.aliases.data(), htons(service.port), + CopyServiceText(lookup.protocol, service.protocol)}; + return &lookup.entry; } return nullptr; } From 6f0f7c7461616ddebf6f026def8b8eb75d04f2f7 Mon Sep 17 00:00:00 2001 From: Aarav <211792885+AaravMehta-07@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:34:24 +0530 Subject: [PATCH 084/353] fix(relinker): reject relocatable ELF conversion inputs --- .../relinker/src/pipeline/RelinkerPipeline.cpp | 9 +++++++++ core/relinker/relinker/tests/test_elf_header.py | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2baf1b7130..eb02d14828 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -16,6 +16,12 @@ namespace Relinker { using namespace Elfpatcher; +namespace { + +constexpr std::uint16_t kEtRel = 1; + +} + RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel, Domain::IProgress& progress) : elfReader(std::move(elfReader)) , syscallScanner(std::move(syscallScanner)) @@ -45,6 +51,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { } RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf) { + if (elfReader->ReadHeader().Type == kEtRel) + throw RelinkerException("ET_REL relocatable inputs are not supported"); + auto programHeaders = elfReader->ReadProgramHeaders(); std::vector textSection; diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 171c44e1c0..9b42382448 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -95,6 +95,16 @@ def main(): result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 2 and error in result.stderr and not output.exists(), (name, result) + + relocatable = main_fixture() + struct.pack_into(" Date: Tue, 13 Oct 2026 13:14:28 +0200 Subject: [PATCH 085/353] fix(agc): sample 10_10_10_2 SNorm and SInt textures Guest texture formats 51 (SNorm) and 55 (SInt) of the 10-bit packed group were missing from the texture lookup and the descriptor format table, so a descriptor using them threw 'unsupported guest texture format' while the UNorm (50) and UInt (54) siblings worked. Map them to A2B10G10R10_SNORM_PACK32 / A2B10G10R10_SINT_PACK32, classify them for sampling (float / signed integer), and list SINT among the integer formats. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp | 9 +++++++++ .../recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp | 4 +++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d4e73c7410..d83ff3a757 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -817,7 +817,7 @@ bool IntegerFormat(VkFormat format) { switch (format) { case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT: - case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: + case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index fdee455cfb..ca7f4da8b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -42,7 +42,9 @@ constexpr FormatEntry kFormatLookup[] = { {29, VK_FORMAT_R16G16_SFLOAT, 4, false}, {36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false}, {50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false}, + {51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false}, {54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false}, + {55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false}, {56, VK_FORMAT_R8G8B8A8_UNORM, 4, false}, {57, VK_FORMAT_R8G8B8A8_SNORM, 4, false}, {60, VK_FORMAT_R8G8B8A8_UINT, 4, false}, diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 33d1a9f457..6c97ab5cdc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -153,6 +153,15 @@ void RunTextureFormatTests() { Require(!IsBlockCompressed(1), "format 1 must not be block compressed"); Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block"); + Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM"); + Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM"); + Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT"); + Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT"); + for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer"); + Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM"); Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide"); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 04f2b9e074..c3d1a68701 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,7 +13,7 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, true, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, @@ -40,7 +40,9 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format11_11_10Float, true, false, false}, {IrBufferFormat::Format10_11_11Float, true, true, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, + {IrBufferFormat::Format10_10_10_2SNorm, true, false, false}, {IrBufferFormat::Format10_10_10_2UInt, true, true, false}, + {IrBufferFormat::Format10_10_10_2SInt, true, false, true}, {IrBufferFormat::Format8_8_8_8UNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8SNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8UInt, true, true, false}, From 0afa191660217fd2abecfec00cc661956171da2b Mon Sep 17 00:00:00 2001 From: melihcan Date: Tue, 13 Oct 2026 14:59:08 +0200 Subject: [PATCH 086/353] test(libSceConvertKeycode): cover sceConvertKeycodeGetImeKeyboardType Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceConvertKeycode/CMakeLists.txt | 15 ++++++++++ core/libs/tests/GuestConvertKeycode.cpp | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 core/libs/prx/libSceConvertKeycode/CMakeLists.txt create mode 100644 core/libs/tests/GuestConvertKeycode.cpp diff --git a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt new file mode 100644 index 0000000000..d4f82eb5f7 --- /dev/null +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceConvertKeycode SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceConvertKeycode PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceConvertKeycode) + +if(BUILD_TESTING) + add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp) + target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libc) + configure_windows_unwind(guest_convert_keycode_tests) + add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests) + if(WIN32) + set_tests_properties(guest_convert_keycode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp new file mode 100644 index 0000000000..c25a9f31bd --- /dev/null +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -0,0 +1,30 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + constexpr int invalidAddress = static_cast(0x80BC0031u); + constexpr int invalidUserId = static_cast(0x80BC0010u); + + Require(sceConvertKeycodeGetImeKeyboardType(0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetImeKeyboardType(-1, nullptr) == invalidAddress); + + std::uint32_t type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); + Require(type == 0xFFFFFFFFu); + + Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == 0); + Require(type == 0); + + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); + Require(type == 0); + return 0; +} From e5f0938409e29959e6955b1987ee228c1185a66f Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:00:31 +0200 Subject: [PATCH 087/353] feat(ces): implement sceCesUcsProfileInitSJis1997Cp932 --- core/libs/prx/libSceCesCs/Export.cpp | 5 +++++ core/libs/tests/GuestCes.cpp | 6 ++++++ docs/dev/debt/libSceCesCs.md | 2 ++ 3 files changed, 13 insertions(+) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 489bc94b31..215e941d22 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -115,6 +115,11 @@ const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { return CP1251_PROFILE; } +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { + if (!sheet) return nullptr; + return sheet; +} + int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { (void)profile; if (!context) return CES_ERROR_INVALID_PARAMETER; diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 55bd867420..b37607ad73 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -9,6 +9,7 @@ const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); int APS5_VABI sceCesSbcToUtf8(const std::uint8_t*, std::uint8_t, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8ToSbc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -106,4 +107,9 @@ int main() { char context[256] = {}; Require(sceCesMbcsUcsContextInit(context, cyrillic) == 0); Require(sceCesMbcsUcsContextInit(nullptr, cyrillic) == invalidParameter); + + char sheet[256] = {}; + Require(sceCesUcsProfileInitSJis1997Cp932(sheet) == sheet); + Require(sceCesUcsProfileInitSJis1997Cp932(nullptr) == nullptr); + Require(sceCesMbcsUcsContextInit(context, sheet) == 0); } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 11cddd4a6b..99cf6f6734 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -3,3 +3,5 @@ ### Silent stubs - [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified +- [sceCesUcsProfileInitSJis1997Cp932](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns the sheet argument without writing profile tables to it, and returns null for a null pointer; Shift-JIS conversion tables are unimplemented. The signature `union* sceCesUcsProfileInitSJis1997Cp932(SceCesUcsProfileSheet* sheet)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) + From 6337c8afbd5641c0c92ad3aa773374f4f17c3443 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 14:18:09 +0200 Subject: [PATCH 088/353] fix(shader): update per-invocation LDS without atomics Pixel, vertex-path and fetch programs keep LDS in a Function-storage array private to each invocation. Sub-dword LDS writes (ds_write_b8, ds_write_b16 and their _d16_hi forms), the 32-bit LDS atomics and ds_append/ds_consume still emitted OpAtomicLoad, OpAtomicCompareExchange or OpAtomicIAdd on that array, with Workgroup scope and a Workgroup OpMemoryBarrier. Vulkan allows atomics only on Uniform, Workgroup, Image, StorageBuffer, PhysicalStorageBuffer and TaskPayloadWorkgroupEXT pointers (VUID-StandaloneSpirv-None-04686) and Workgroup memory scope only in compute, mesh, task and tessellation control (VUID-StandaloneSpirv-None-07321). With ANYPS5_ENABLE_SPIRV_TOOLS the recompiler rejected such shaders; without it the invalid module went to the driver. InFunctionStorage now names the storage EmitMemoryElementPointer picks. For Function storage, StoreSubwordInBounds, Atomic32, EmitAtomicUpdate, SharedAtomicUpdate and AppendConsume load the dword, compute the new value and store it, as scratch already did. The array belongs to one invocation, so no other invocation can observe the dword in between. Atomic32 takes the next value from AtomicNext32, which FlatAtomicNext32 now shares for its plain integer operations. The per-invocation LDS model and what it means for ds_append are recorded as technical debt. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvMemory/SpirvBufferAccess.hpp | 1 + .../src/SpirvMemory/SpirvBufferAccess.cpp | 7 +- .../SpirvMemory/SpirvMemoryInstructions.cpp | 76 ++++++++++++++----- docs/dev/debt/shader.md | 1 + 5 files changed, 66 insertions(+), 20 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 7d861c62c2..3c16d82fd9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -108,6 +108,7 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); 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); +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index c89d6a2112..03bd0330d6 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -18,6 +18,7 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); 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); +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index b0c4e7b325..44745b1c8b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -168,13 +168,18 @@ std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryRe return inBounds; } +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) { + if (kind == ResourceKind::Scratch) return true; + return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr; +} + std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); } if (access.kind == ResourceKind::Lds || access.kind == ResourceKind::Scratch) { const auto pointer = state.module.AllocateId(); - const std::uint32_t storageClass = access.kind == ResourceKind::Scratch ? spv::StorageClassFunction : ShaderWorkgroupInput(state) != nullptr ? spv::StorageClassWorkgroup : spv::StorageClassFunction; + const std::uint32_t storageClass = InFunctionStorage(state, access.kind) ? spv::StorageClassFunction : spv::StorageClassWorkgroup; state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, storageClass), pointer, access.objectPointer, index); return pointer; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 64ae0cd782..88cd12c7b2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -231,7 +231,7 @@ void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, con const auto merge = [&](std::uint32_t old) { return Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), old, Unary(state, spv::OpNot, TypeU32(state), mask)), value); }; - if (mem.kind == ResourceKind::Scratch) { + if (InFunctionStorage(state, mem.kind)) { const auto old = state.module.AllocateId(); if (resource.memoryAccess != 0u) { state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer, resource.memoryAccess); @@ -642,6 +642,47 @@ std::uint32_t EmitAtomicOperation(SpirvValueEmitContext& ctx, const IrValue& ins return old; } +std::uint32_t AtomicNext32(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { + const auto u32 = TypeU32(state); + const auto winner = [&](spv::Op compare) { + return Select(state, u32, Binary(state, compare, TypeBool(state), old, value), old, value); + }; + switch (opcode) { + case spv::OpAtomicExchange: + return value; + case spv::OpAtomicCompareExchange: + return Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); + case spv::OpAtomicIAdd: + return Binary(state, spv::OpIAdd, u32, old, value); + case spv::OpAtomicISub: + return Binary(state, spv::OpISub, u32, old, value); + case spv::OpAtomicSMin: + return winner(spv::OpSLessThan); + case spv::OpAtomicUMin: + return winner(spv::OpULessThan); + case spv::OpAtomicSMax: + return winner(spv::OpSGreaterThan); + case spv::OpAtomicUMax: + return winner(spv::OpUGreaterThan); + case spv::OpAtomicAnd: + return Binary(state, spv::OpBitwiseAnd, u32, old, value); + case spv::OpAtomicOr: + return Binary(state, spv::OpBitwiseOr, u32, old, value); + case spv::OpAtomicXor: + return Binary(state, spv::OpBitwiseXor, u32, old, value); + default: + throw std::runtime_error("AtomicNext32: opcode has no 32-bit replacement"); + } +} + +template +std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t pointer, TReplacement&& replacement) { + const auto old = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, replacement(old)); + return old; +} + template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); @@ -667,9 +708,11 @@ template std::uint32_t EmitAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, TReplacement&& replacement) { const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { + const auto next = [&](std::uint32_t old) { return replacement(ctx.state, old, value); - }); + }; + if (InFunctionStorage(ctx.state, mem.kind)) return UpdateFunctionElement(ctx.state, pointer, next); + return AtomicUpdate(ctx.state, pointer, mem.kind, next); }); } @@ -738,6 +781,10 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me } if (mem.kind == ResourceKind::Buffer) NoteBufferAtomicSite(zeroSkip, zeroIdentity && !zeroSkip); const auto apply = [&](std::uint32_t pointer) { + if (InFunctionStorage(state, mem.kind)) { + const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); + return UpdateFunctionElement(state, pointer, [&](std::uint32_t old) { return AtomicNext32(state, SpirvAtomicOpcode(inst.Opcode()), old, value, 0u); }); + } const auto operation = [&]() { const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { @@ -857,7 +904,9 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst auto& state = ctx.state; const auto& mem = SharedMemory(ctx, inst); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - const auto old = AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { return replacement(state, current); }); + const auto next = [&](std::uint32_t current) { return replacement(state, current); }; + if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); + const auto old = AtomicUpdate(state, pointer, mem.kind, next); const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); return old; @@ -914,33 +963,19 @@ std::uint32_t AddressAtomicOpcode(IrOpcode opcode) { } std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { - const auto u32 = TypeU32(state); - const auto winner = [&](spv::Op compare) { - return Select(state, u32, Binary(state, compare, TypeBool(state), old, value), old, value); - }; switch (opcode) { case IrOpcode::AddressAtomicSwap32: - return value; case IrOpcode::AddressAtomicCmpSwap32: - return Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); case IrOpcode::AddressAtomicIAdd32: - return Binary(state, spv::OpIAdd, u32, old, value); case IrOpcode::AddressAtomicISub32: - return Binary(state, spv::OpISub, u32, old, value); case IrOpcode::AddressAtomicSMin32: - return winner(spv::OpSLessThan); case IrOpcode::AddressAtomicUMin32: - return winner(spv::OpULessThan); case IrOpcode::AddressAtomicSMax32: - return winner(spv::OpSGreaterThan); case IrOpcode::AddressAtomicUMax32: - return winner(spv::OpUGreaterThan); case IrOpcode::AddressAtomicAnd32: - return Binary(state, spv::OpBitwiseAnd, u32, old, value); case IrOpcode::AddressAtomicOr32: - return Binary(state, spv::OpBitwiseOr, u32, old, value); case IrOpcode::AddressAtomicXor32: - return Binary(state, spv::OpBitwiseXor, u32, old, value); + return AtomicNext32(state, AddressAtomicOpcode(opcode), old, value, comparator); case IrOpcode::AddressAtomicInc32: return AtomicIncrement(state, old, value); case IrOpcode::AddressAtomicDec32: @@ -1176,6 +1211,9 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { + if (InFunctionStorage(state, mem.kind)) { + return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); + } const auto value = state.module.AllocateId(); state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); return value; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e0109d65cc..28ba64d6a5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -94,6 +94,7 @@ - [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. - [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), and `ds_add_f32`, which keeps the host's `OpFAdd`. +- [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 and atomics throw. - [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. From 16f63d8eddc662910d45f43a307d56c9ffe6490d Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 14:18:10 +0200 Subject: [PATCH 089/353] test(agc): cover per-invocation LDS read-modify-writes agc_shader_memory recompiles pixel and vertex programs in wave64 and wave32 with ds_write_b8, ds_write_b16, ds_write_b8_d16_hi, ds_write_b16_d16_hi, ds_add_u32, ds_inc_rtn_u32, ds_mskor_b32 and ds_append, and requires that no atomic uses a Function pointer and that no atomic or memory barrier uses Workgroup memory scope. With SPIR-V tools enabled each of them failed validation with VUID-StandaloneSpirv-None-04686 before the fix. agc_driver_pixel_lds_read_modify_write draws a 64x8 target in wave64 and wave32. Each pixel uses its own 16-byte LDS slot: it writes bytes and halves into initialised dwords, chains ds_add_rtn_u32, ds_inc_rtn_u32 and ds_mskor_rtn_b32 on one dword and reads ds_append from a counter every lane initialised, then stores the dwords and the returned values to a buffer. It needs no subgroup wider than the host's, so it runs on lavapipe. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 76 +++++++ .../execution/PixelLdsReadModifyWrite.cpp | 197 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9eafacd977..2554a475bc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3172,6 +3172,81 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } +void verifyFunctionLdsUpdates() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; + const auto compile = [](ShaderStage stage, std::uint32_t waveSize, std::span body) { + std::vector code{0x7e000280u, 0x7e020292u, 0xbefc03ffu, 0x0000ffffu}; + code.insert(code.end(), body.begin(), body.end()); + code.insert(code.end(), {0xbf8cc07fu, 0xd8d80000u, 0x02000000u, 0xbf8cc07fu, stage == ShaderStage::Fragment ? 0xf800180fu : 0xf80008cfu, 0x02020202u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {stage, 0x40000u, code, 0, {}}; + request.context.waveSize = waveSize; + if (stage == ShaderStage::Fragment) { + ShaderPixelStageInfo info{}; + info.wave32 = waveSize == 32u; + info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + info.hasPerspectiveCenterVgpr = true; + info.targetOutputMode[0] = 9u; + info.targetExportMapping.fill(0xe4u); + request.context.pixel = info; + } else { + request.context.vertex = ShaderVertexStageInfo{}; + } + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto violations = [](const std::vector& words) { + std::map storage; + std::map constants; + std::map types; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpTypePointer) storage[words[at + 1]] = words[at + 2]; + if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + if (op == spv::OpVariable || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) types[words[at + 2]] = words[at + 1]; + } + const auto workgroupScope = [&](std::uint32_t id) { return constants.count(id) != 0 && constants.at(id) == spv::ScopeWorkgroup; }; + std::size_t found = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpMemoryBarrier && workgroupScope(words[at + 1])) ++found; + if ((op < spv::OpAtomicLoad || op > spv::OpAtomicXor) && op != spv::OpAtomicFMinEXT && op != spv::OpAtomicFMaxEXT && op != spv::OpAtomicFAddEXT) continue; + const auto pointer = words[at + (op == spv::OpAtomicStore ? 1u : 3u)]; + const auto type = types.find(pointer); + if (type != types.end() && storage.count(type->second) != 0 && storage.at(type->second) == spv::StorageClassFunction) ++found; + if (workgroupScope(words[at + (op == spv::OpAtomicStore ? 2u : 4u)])) ++found; + } + return found; + }; + using Program = std::pair>; + static constexpr std::array programs{{ + {"ds_write_b8", {0xd8780000u, 0x00000100u}}, + {"ds_write_b16", {0xd87c0000u, 0x00000100u}}, + {"ds_write_b8_d16_hi", {0xda800000u, 0x00000100u}}, + {"ds_write_b16_d16_hi", {0xda840000u, 0x00000100u}}, + {"ds_add_u32", {0xd8000000u, 0x00000100u}}, + {"ds_inc_rtn_u32", {0xd88c0000u, 0x03000100u}}, + {"ds_mskor_b32", {0xd8300000u, 0x00010100u}}, + {"ds_append", {0xd8f80000u, 0x03000000u}}, + }}; + for (const auto& [name, body] : programs) { + for (const auto stage : {ShaderStage::Fragment, ShaderStage::Vertex}) { + for (const auto waveSize : {64u, 32u}) { + const auto what = std::string("function LDS: ") + name + (stage == ShaderStage::Fragment ? " in a pixel" : " in a vertex") + " wave" + std::to_string(waveSize) + " program"; + const auto result = compile(stage, waveSize, body); + require(!result.spirv.empty(), (what + " did not compile").c_str()); + require(violations(result.spirv.Words()) == 0, (what + " uses an atomic on a Function pointer or a Workgroup memory scope").c_str()); + } + } + } +} + void verifySrtReadPatching() { using namespace ShaderRecompiler; IrProgram program; @@ -3533,6 +3608,7 @@ int main(int argc, char** argv) { verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); verifyFunctionLdsBound(); + verifyFunctionLdsUpdates(); verifySrtReadPatching(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp new file mode 100644 index 0000000000..17a71bd6ff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp @@ -0,0 +1,197 @@ +#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 Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Factor = 0x00a5c3e1u; +constexpr std::uint32_t Counter = 0x1234u; +constexpr std::uint32_t CounterOffset = 0x2000u; +alignas(256) std::array Pixels{}; +alignas(256) std::array Outputs{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +std::vector PixelCode() { + std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; + const auto vop2 = [&](std::uint32_t opcode, std::uint32_t destination, std::uint32_t source, std::uint32_t vector) { + code.push_back(opcode | (destination << 17u) | (vector << 9u) | source); + }; + const auto ds = [&](std::uint32_t opcode, std::uint32_t offset, std::uint32_t address, std::uint32_t data0, std::uint32_t data1, std::uint32_t destination) { + code.push_back(0xd8000000u | (opcode << 18u) | offset); + code.push_back(address | (data0 << 8u) | (data1 << 16u) | (destination << 24u)); + }; + vop2(0x36000000, 8, 0xff, 6); + code.push_back(Width * Height - 1u); + vop2(0x34000000, 8, 0x84, 8); + code.push_back(0x7e126f06); + vop2(0x16000000, 10, 0xff, 6); + code.push_back(Factor); + code.insert(code.end(), {0x7e1c02c1, 0x7e2002ff, 0xffff0000, 0x7e240280, 0x7e2602ff, Counter, 0xbefc03ff, 0x0000ffff}); + for (std::uint32_t offset = 0; offset < 16; offset += 4) ds(0x0d, offset, 8, 9, 0, 0); + ds(0x1e, 0, 8, 10, 0, 0); + ds(0x1e, 2, 8, 6, 0, 0); + ds(0xa1, 6, 8, 10, 0, 0); + ds(0x1f, 8, 8, 6, 0, 0); + ds(0xa0, 11, 8, 10, 0, 0); + ds(0x20, 12, 8, 10, 0, 12); + ds(0x23, 12, 8, 14, 0, 13); + ds(0x2c, 12, 8, 16, 6, 15); + ds(0x0d, CounterOffset, 18, 19, 0, 0); + ds(0x3e, CounterOffset, 0, 0, 0, 17); + code.push_back(0xbf8cc07f); + for (std::uint32_t dword = 0; dword < 4; ++dword) ds(0x36, dword * 4u, 8, 0, 0, 20 + dword); + code.push_back(0xbf8cc07f); + const std::array stored{20, 21, 22, 23, 12, 13, 15, 17}; + for (std::uint32_t slot = 0; slot < Results; ++slot) { + code.push_back(0xe0702000 | (slot * 4u)); + code.push_back(0x80010006 | (stored[slot] << 8u)); + } + code.insert(code.end(), {0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}); + return code; +} + +std::array Expected(std::uint32_t pixel) { + const auto initial = ~pixel; + const auto product = static_cast(std::uint64_t{Factor} * pixel); + const auto added = initial + product; + const auto incremented = added == 0xffffffffu ? 0u : added + 1u; + const auto masked = (incremented & 0x0000ffffu) | pixel; + return { + (initial & 0xff00ff00u) | (product & 0xffu) | ((pixel & 0xffu) << 16u), + (initial & 0x0000ffffu) | (product & 0xffff0000u), + (initial & 0x00ff0000u) | (pixel & 0xffffu) | (((product >> 16u) & 0xffu) << 24u), + masked, + initial, + added, + incremented, + Counter, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +constexpr std::array, 3> Triangles{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + const auto& triangles = Triangles; + const auto pixelCode = PixelCode(); + Pixels.fill(std::byte{0}); + Outputs.fill(Sentinel); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Outputs.data(), Results * 4u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize) { + static constexpr std::array names{"ds_write_b8 dword", "ds_write_b16_d16_hi dword", "ds_write_b16 and ds_write_b8_d16_hi dword", "atomic dword", "ds_add_rtn_u32", "ds_inc_rtn_u32", "ds_mskor_rtn_b32", "ds_append"}; + const auto what = "pixel LDS read-modify-write wave" + std::to_string(waveSize); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Pixels[pixel * 4u + channel] == std::byte{255}, what + ": missing color at pixel " + std::to_string(pixel)); + } + const auto expected = Expected(pixel); + for (std::uint32_t slot = 0; slot < Results; ++slot) { + const auto actual = Outputs[pixel * Results + slot]; + Require(actual == expected[slot], what + ": pixel " + std::to_string(pixel) + " " + names[slot] + " expected " + std::to_string(expected[slot]) + " got " + std::to_string(actual)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {64u, 32u}) { + std::fprintf(stderr, "pixel LDS read-modify-write wave%u\n", waveSize); + for (std::uint32_t pass = 0; pass < 2; ++pass) { + Draw(*device, waveSize); + Check(waveSize); + } + } + std::puts("pixel LDS read-modify-write tests passed: 4 draws, 2048 pixels, 16384 results"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2f453805aa58682f492353b14cb5d5eee55f4d31 Mon Sep 17 00:00:00 2001 From: Aarav <211792885+AaravMehta-07@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:33:18 +0530 Subject: [PATCH 090/353] fix(libSceSystemService): contain converted executable paths --- .../prx/libSceSystemService/CMakeLists.txt | 11 +++ core/libs/prx/libSceSystemService/Export.cpp | 8 ++ .../tests/GuestSystemServicePathBoundary.cpp | 82 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 core/libs/tests/GuestSystemServicePathBoundary.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 9b90ef5883..ea197fc406 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -3,6 +3,17 @@ target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) + if(WIN32) + add_test_executable(guest_system_service_path_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServicePathBoundary.cpp) + target_include_directories(guest_system_service_path_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_path_boundary_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_path_boundary_tests) + add_test(NAME guest_system_service_path_boundary COMMAND guest_system_service_path_boundary_tests) + set_tests_properties(guest_system_service_path_boundary PROPERTIES TIMEOUT 20) + set_tests_properties(guest_system_service_path_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_system_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9bdc3e6cac..9fed682ae0 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -65,6 +65,12 @@ std::string ConvertedExecutableName(const char* path) { } } +void RequireExecutableBeside(const std::filesystem::path& directory, const std::filesystem::path& target, const char* guestPath) { + if (target.lexically_normal().parent_path() != directory.lexically_normal()) { + UnconvertedExecutable(std::string("an executable outside /app0 (") + guestPath + ")"); + } +} + #if defined(_WIN32) std::wstring Utf16(const std::string& text) { if (text.empty()) return {}; @@ -115,6 +121,7 @@ HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { launch.executable = mainLength > 0 && mainLength < main.size() ? main.substr(0, mainLength) : own; } else { const auto target = ownPath.parent_path() / (Utf16(ConvertedExecutableName(path)) + ownPath.extension().wstring()); + RequireExecutableBeside(ownPath.parent_path(), target, path); if (!std::filesystem::is_regular_file(target)) { UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); } @@ -156,6 +163,7 @@ HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { launch.executable = main != nullptr && *main != '\0' ? main : own.string(); } else { const auto target = own.parent_path() / (ConvertedExecutableName(path) + own.extension().string()); + RequireExecutableBeside(own.parent_path(), target, path); if (!std::filesystem::is_regular_file(target)) { UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); } diff --git a/core/libs/tests/GuestSystemServicePathBoundary.cpp b/core/libs/tests/GuestSystemServicePathBoundary.cpp new file mode 100644 index 0000000000..da8e6927f0 --- /dev/null +++ b/core/libs/tests/GuestSystemServicePathBoundary.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +[[noreturn]] void UnexpectedLaunch() { + std::_Exit(86); +} + +int Probe(const char* name) { + LibcRegisterShutdown_nid_postfix(UnexpectedLaunch); + const std::string path = std::string("/app0/..\\") + name; + try { + sceSystemServiceLoadExec(path.c_str(), nullptr); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(path) != std::string::npos ? 0 : 2; + } + return 1; +} + +DWORD RunProbe(const std::filesystem::path& executable, const std::string& name) { + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + std::wstring command = L"\"" + executable.wstring() + L"\" --probe "; + command.append(name.begin(), name.end()); + Require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + const DWORD waitResult = WaitForSingleObject(process.hProcess, 10000); + if (waitResult != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, ERROR_TIMEOUT); + WaitForSingleObject(process.hProcess, 5000); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return ERROR_TIMEOUT; + } + DWORD code = 1; + if (!GetExitCodeProcess(process.hProcess, &code)) code = ERROR_GEN_FAILURE; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return code; +} + +int Test() { + std::wstring executable(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length > 0 && length < executable.size()); + executable.resize(length); + const std::filesystem::path own(executable); + const std::string name = "anyps5-path-boundary-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64()); + const auto fixture = own.parent_path().parent_path() / (name + own.extension().string()); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { + std::error_code error; + std::filesystem::remove(path, error); + } + } cleanup{fixture}; + std::filesystem::copy_file(own, fixture); + Require(RunProbe(own, name) == 0); + return 0; +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--probe") == 0) return Probe(argv[2]); + Require(argc == 1); + return Test(); +} From 79e162110aa4bae544acb10ebf2cb763a84d20be Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:06:13 +0200 Subject: [PATCH 091/353] fix(libkernel): report SO_ERROR and the other SOL_SOCKET options like FreeBSD getsockopt and setsockopt on a libkernel socket knew four SOL_SOCKET options (SO_REUSEADDR, SO_BROADCAST, SO_SNDBUF, SO_RCVBUF) and failed every other one with ENOPROTOOPT. The Mound's EOS SDK reads SO_ERROR on two of its sockets after a connect, which therefore always failed. The SOL_SOCKET options now follow FreeBSD 11.4 sosetopt and sogetopt: - SO_ERROR returns the pending error of the socket as a FreeBSD errno and reports it once (Winsock does not clear it on read, so the library does). - SO_TYPE, SO_ACCEPTCONN, SO_KEEPALIVE, SO_DONTROUTE, SO_OOBINLINE and SO_LINGER are read from and set on the host socket; the boolean options read back as their own bit, as FreeBSD returns so_options & option. - SO_DEBUG, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP and SO_NOSIGPIPE stay ENOPROTOOPT until a title needs one. - A value is read from its first int and fails with EINVAL when shorter, a getsockopt buffer is filled up to its length (sooptcopyin, sooptcopyout), SO_SNDBUF and SO_RCVBUF below 1 fail with EINVAL, and SO_LINGER outside 0..65535 seconds fails with EDOM. A connect on a non-blocking socket now fails with EINPROGRESS on Windows too (it was EWOULDBLOCK, which a title waiting for the connection does not expect). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 11 + .../Socket/include/SocketOptions.hpp | 13 + .../libkernel/Socket/src/SocketOptions.cpp | 150 ++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 47 ++-- core/libs/tests/GuestSocketOptions.cpp | 227 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 424 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libkernel/Socket/include/SocketOptions.hpp create mode 100644 core/libs/prx/libkernel/Socket/src/SocketOptions.cpp create mode 100644 core/libs/tests/GuestSocketOptions.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 798d82118e..35aac28ab7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp @@ -984,6 +985,16 @@ if(BUILD_TESTING) target_include_directories(guest_socket_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_socket_error_tests) add_test(NAME guest_socket_error COMMAND guest_socket_error_tests) + add_test_executable(guest_socket_options_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOptions.cpp) + target_include_directories(guest_socket_options_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_options_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_options_tests) + add_test(NAME guest_socket_options COMMAND guest_socket_options_tests) + set_tests_properties(guest_socket_options PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_options PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp new file mode 100644 index 0000000000..d41d47b63c --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +namespace GuestSockets { + +struct SocketOptionState { + int reportedError = 0; +}; + +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); +int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); + +} diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp new file mode 100644 index 0000000000..747bf83a97 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -0,0 +1,150 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" +#include +#include + +namespace GuestSockets { +namespace { + +constexpr int GuestAcceptConnection = 0x2; +constexpr int GuestReuseAddress = 0x4; +constexpr int GuestKeepAlive = 0x8; +constexpr int GuestDontRoute = 0x10; +constexpr int GuestBroadcast = 0x20; +constexpr int GuestLinger = 0x80; +constexpr int GuestOobInline = 0x100; +constexpr int GuestSendBuffer = 0x1001; +constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestError = 0x1007; +constexpr int GuestType = 0x1008; + +constexpr int ErrorInvalid = 22; +constexpr int ErrorDomain = 33; +constexpr int ErrorNoProtocolOption = 42; +constexpr int MaximumLinger = 65535; + +struct GuestLingerValue { + std::int32_t enabled; + std::int32_t seconds; +}; + +#ifdef _WIN32 +using NativeHandle = SOCKET; +using NativeOptionValue = const char*; +using NativeOptionOutput = char*; +int HostError() { return GuestErrorFromHost(WSAGetLastError()); } +#else +using NativeHandle = int; +using NativeOptionValue = const void*; +using NativeOptionOutput = void*; +int HostError() { return GuestErrorFromHost(errno); } +#endif + +int NativeBoolean(int option) { + switch (option) { + case GuestReuseAddress: return SO_REUSEADDR; + case GuestKeepAlive: return SO_KEEPALIVE; + case GuestDontRoute: return SO_DONTROUTE; + case GuestBroadcast: return SO_BROADCAST; + case GuestOobInline: return SO_OOBINLINE; + default: return -1; + } +} + +int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { + const std::uint32_t count = std::min(*length, size); + if (count != 0 && !output) return ErrorInvalid; + if (count != 0) std::memcpy(output, value, count); + *length = count; + return 0; +} + +} + +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length) { + const auto handle = static_cast(native); + if (option == GuestLinger) { + if (length < sizeof(GuestLingerValue)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestLingerValue guest; + std::memcpy(&guest, value, sizeof(guest)); + if (guest.seconds < 0 || guest.seconds > MaximumLinger) return ErrorDomain; +#ifdef _WIN32 + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = static_cast(guest.seconds); +#else + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = guest.seconds; +#endif + if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); + return 0; + } + const bool native_boolean = NativeBoolean(option) != -1; + if (native_boolean || option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (length < sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (number < 1) return ErrorInvalid; + const int nativeOption = option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&number), sizeof(number))) return HostError(); + return 0; + } + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(handle, SOL_SOCKET, NativeBoolean(option), reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; + } + return ErrorNoProtocolOption; +} + +int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { + const auto handle = static_cast(native); + if (option == GuestLinger) { + linger current{}; + socklen_t size = sizeof(current); + if (::getsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(¤t), &size)) return HostError(); + const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; + return Copy(value, length, &guest, sizeof(guest)); + } + int number = 0; + if (const int nativeOption = NativeBoolean(option); nativeOption != -1) { + int enabled = 0; + socklen_t size = sizeof(enabled); + if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); + number = enabled != 0 ? option : 0; + } else if (option == GuestAcceptConnection) { + int listening = 0; + socklen_t size = sizeof(listening); + if (::getsockopt(handle, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size)) return HostError(); + number = listening != 0 ? GuestAcceptConnection : 0; + } else if (option == GuestType) { + number = type; + } else if (option == GuestError) { + int pending = 0; + socklen_t size = sizeof(pending); + if (::getsockopt(handle, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size)) return HostError(); +#ifdef _WIN32 + if (pending != 0 && pending == state.reportedError) pending = 0; + else if (pending != 0) state.reportedError = pending; +#endif + number = pending == 0 ? 0 : GuestErrorFromHost(pending); + } else if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + socklen_t size = sizeof(number); + if (::getsockopt(handle, SOL_SOCKET, option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&number), &size)) return HostError(); + } else { + return ErrorNoProtocolOption; + } + return Copy(value, length, &number, sizeof(number)); +} + +} diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7a0304ce10..3e63b1f2ff 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -19,6 +19,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -67,6 +68,12 @@ int NativeError() { return GuestSockets::GuestErrorFromHost(errno); #endif } +int ConnectError() { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) return 36; +#endif + return NativeError(); +} int SendError(int type) { #ifdef _WIN32 if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; @@ -81,6 +88,7 @@ struct Socket { int type; std::mutex modeMutex; bool nonblocking = false; + GuestSockets::SocketOptionState options; Socket(NativeSocket value, int family, int type) : value(value), family(family), type(type) {} ~Socket() { if (value == Invalid) return; @@ -187,15 +195,6 @@ std::shared_ptr Lookup(int descriptor) { Fail(9); return {}; } -int Option(int guest) { - switch (guest) { - case 0x4: return SO_REUSEADDR; - case 0x20: return SO_BROADCAST; - case 0x1001: return SO_SNDBUF; - case 0x1002: return SO_RCVBUF; - default: return -1; - } -} bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { if (!input || length < 2) { Fail(14); return false; } const auto* bytes = static_cast(input); @@ -732,26 +731,20 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } const auto socket = Lookup(descriptor); if (!socket) return -1; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || length < sizeof(int)) return Fail(22); - return ::setsockopt(socket->value, SOL_SOCKET, nativeOption, - static_cast(value), sizeof(int)) ? Fail(NativeError()) : 0; + if (level != 0xffff) return Fail(42); + std::lock_guard lock(socket->modeMutex); + const int error = GuestSockets::SetSocketLevelOption(static_cast(socket->value), socket->options, option, value, length); + return error == 0 ? 0 : Fail(error); } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || !length || *length < sizeof(int)) return Fail(22); - int result = 0; - socklen_t size = sizeof(result); - if (::getsockopt(socket->value, SOL_SOCKET, nativeOption, reinterpret_cast(&result), &size)) - return Fail(NativeError()); - std::memcpy(value, &result, sizeof(result)); - *length = sizeof(result); - return 0; + if (level != 0xffff) return Fail(42); + if (!length) return Fail(22); + std::lock_guard lock(socket->modeMutex); + const int error = GuestSockets::GetSocketLevelOption(static_cast(socket->value), socket->options, socket->type, option, value, length); + return error == 0 ? 0 : Fail(error); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { const bool nonblocking = (type & GuestNonBlocking) != 0; @@ -828,7 +821,11 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint socklen_t size; if (!Address(address, length, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); - return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; + { + std::lock_guard lock(socket->modeMutex); + socket->options.reportedError = 0; + } + return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(ConnectError()) : 0; } int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp new file mode 100644 index 0000000000..c9b97d1b4a --- /dev/null +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -0,0 +1,227 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI poll_nid_postfix(void*, std::uint32_t, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int SolSocket = 0xffff; +constexpr int SoAcceptConnection = 0x2; +constexpr int SoDebug = 0x1; +constexpr int SoKeepAlive = 0x8; +constexpr int SoBroadcast = 0x20; +constexpr int SoUseLoopback = 0x40; +constexpr int SoLinger = 0x80; +constexpr int SoReusePort = 0x200; +constexpr int SoTimestamp = 0x400; +constexpr int SoNoSigpipe = 0x800; +constexpr int SoSendBuffer = 0x1001; +constexpr int SoError = 0x1007; +constexpr int SoType = 0x1008; +constexpr int ErrorInvalid = 22; +constexpr int ErrorDomain = 33; +constexpr int ErrorNoProtocolOption = 42; +constexpr int ErrorConnectionRefused = 61; +constexpr int ErrorBadDescriptor = 9; + +struct Linger { + std::int32_t enabled; + std::int32_t seconds; +}; + +struct PollDescriptor { + std::int32_t descriptor; + std::int16_t events; + std::int16_t revents; +}; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +int GetInt(int descriptor, int option) { + int value = -12345; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(descriptor, SolSocket, option, &value, &size) == 0 && size == sizeof(value), "getsockopt of an integer option failed"); + return value; +} + +void SetInt(int descriptor, int option, int value) { + Require(setsockopt_nid_postfix(descriptor, SolSocket, option, &value, sizeof(value)) == 0, "setsockopt of an integer option failed"); +} + +bool SetFails(int descriptor, int level, int option, const void* value, std::uint32_t length, int error) { + return setsockopt_nid_postfix(descriptor, level, option, value, length) == -1 && *__error_nid_postfix() == error; +} + +std::array Loopback(std::uint16_t portNetworkOrder) { + std::array address{16, 2}; + std::memcpy(address.data() + 2, &portNetworkOrder, 2); + address[4] = 127; + address[7] = 1; + return address; +} + +std::array Listen(int* listener) { + *listener = socket_nid_postfix(2, Stream, 0); + Require(*listener >= 0, "socket failed"); + auto address = Loopback(0); + Require(bind_nid_postfix(*listener, address.data(), address.size()) == 0, "bind failed"); + Require(listen_nid_postfix(*listener, 4) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(*listener, address.data(), &size) == 0, "getsockname failed"); + return address; +} + +void WaitUntilSettled(int descriptor) { + PollDescriptor entry{descriptor, 0x1 | 0x4, 0}; + Require(poll_nid_postfix(&entry, 1, 6000) == 1, "the nonblocking connect did not settle within 6 s"); +} + +void ReportsTypeAndListening() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + Require(GetInt(stream, SoType) == Stream, "SO_TYPE of a stream socket must be SOCK_STREAM (1)"); + Require(GetInt(datagram, SoType) == Datagram, "SO_TYPE of a datagram socket must be SOCK_DGRAM (2)"); + Require(GetInt(stream, SoAcceptConnection) == 0, "SO_ACCEPTCONN must be 0 before listen"); + auto address = Loopback(0); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0 && listen_nid_postfix(stream, 4) == 0, "bind or listen failed"); + Require(GetInt(stream, SoAcceptConnection) == SoAcceptConnection, "SO_ACCEPTCONN of a listener is the option bit (2), as FreeBSD returns so_options & SO_ACCEPTCONN"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void ReportsAndClearsTheConnectError() { + int listener = -1; + auto address = Listen(&listener); + const int client = socket_nid_postfix(2, Stream, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0, "nonblocking socket setup failed"); + const int result = connect_nid_postfix(client, address.data(), address.size()); + Require(result == 0 || (result == -1 && *__error_nid_postfix() == 36), "nonblocking connect to a listener must succeed or be in progress"); + WaitUntilSettled(client); + Require(GetInt(client, SoError) == 0, "SO_ERROR after a successful connect must be 0"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0, "close failed"); + + int closed = -1; + auto refused = Listen(&closed); + Require(close_nid_postfix(closed) == 0, "close failed"); + const int failing = socket_nid_postfix(2, Stream, 0); + Require(failing >= 0 && fcntl_nid_postfix(failing, 4, 4) == 0, "nonblocking socket setup failed"); + const int attempt = connect_nid_postfix(failing, refused.data(), refused.size()); + if (attempt == -1 && *__error_nid_postfix() != 36) { + Require(*__error_nid_postfix() == ErrorConnectionRefused, "a refused connect must fail with ECONNREFUSED (61)"); + } else { + WaitUntilSettled(failing); + Require(GetInt(failing, SoError) == ErrorConnectionRefused, "SO_ERROR after a refused connect must be ECONNREFUSED (61)"); + Require(GetInt(failing, SoError) == 0, "reading SO_ERROR must clear it"); + } + Require(close_nid_postfix(failing) == 0, "close failed"); +} + +void BooleanOptionsReportTheirBit() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE is off by default"); + SetInt(stream, SoKeepAlive, 1); + Require(GetInt(stream, SoKeepAlive) == SoKeepAlive, "an enabled boolean option reads back as its own bit (FreeBSD returns so_options & option)"); + SetInt(stream, SoKeepAlive, 0); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE must turn off"); + SetInt(datagram, SoBroadcast, 1); + Require(GetInt(datagram, SoBroadcast) == SoBroadcast, "SO_BROADCAST reads back as its bit"); + for (const int option : {SoNoSigpipe, SoReusePort, SoDebug, SoUseLoopback, SoTimestamp}) { + const int one = 1; + Require(SetFails(stream, SolSocket, option, &one, sizeof(one), ErrorNoProtocolOption), "an option no title needs must fail with ENOPROTOOPT (42)"); + int value = 0; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an option no title needs must fail with ENOPROTOOPT (42)"); + } + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void LingerFollowsFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + Linger linger{1, 5}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER must be accepted"); + Linger read{}; + std::uint32_t size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && size == sizeof(read), "getsockopt SO_LINGER failed"); + Require(read.enabled == SoLinger && read.seconds == 5, "SO_LINGER reads back as {SO_LINGER bit, seconds}"); + linger = {0, 0}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER off must be accepted"); + size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && read.enabled == 0, "SO_LINGER must read back as off"); + linger = {1, -1}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a negative linger time must fail with EDOM (33)"); + linger = {1, 65536}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a linger time above 65535 must fail with EDOM (33)"); + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(std::int32_t), ErrorInvalid), "a linger value shorter than struct linger must fail with EINVAL (22)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void LengthsFollowFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, SoKeepAlive, &one, 2, ErrorInvalid), "an integer option shorter than an int must fail with EINVAL (22)"); + const std::array wide{1}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, wide.data(), sizeof(wide)) == 0, "a value longer than an int is read from its first int"); + std::array narrow{0xaa, 0xaa}; + std::uint32_t size = narrow.size(); + Require(getsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, narrow.data(), &size) == 0 && size == narrow.size(), "a short buffer gets a truncated value and its own length (sooptcopyout)"); + Require(narrow[0] == SoKeepAlive && narrow[1] == 0, "the truncated value is the low bytes of the int"); + const int zero = 0; + Require(SetFails(stream, SolSocket, SoSendBuffer, &zero, sizeof(zero), ErrorInvalid), "SO_SNDBUF below 1 must fail with EINVAL (22)"); + const int negative = -5; + Require(SetFails(stream, SolSocket, SoSendBuffer, &negative, sizeof(negative), ErrorInvalid), "a negative SO_SNDBUF must fail with EINVAL (22)"); + SetInt(stream, SoSendBuffer, 65536); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void UnknownRequestsFail() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, 0x7fff, &one, sizeof(one), ErrorNoProtocolOption), "an unknown SOL_SOCKET option must fail with ENOPROTOOPT (42)"); + int value = 0; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, 0x7fff, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an unknown option must fail with ENOPROTOOPT (42)"); + Require(setsockopt_nid_postfix(-1, SolSocket, SoKeepAlive, &one, sizeof(one)) == -1 && *__error_nid_postfix() == ErrorBadDescriptor, "setsockopt on a bad descriptor must fail with EBADF (9)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ReportsTypeAndListening(); + ReportsAndClearsTheConnectError(); + BooleanOptionsReportTheirBit(); + LingerFollowsFreeBsd(); + LengthsFollowFreeBsd(); + UnknownRequestsFail(); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 03abba3e13..c69897e156 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -144,6 +144,7 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, libkernel `fstat` on a socket throws, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level only, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_NOSIGPIPE`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them) and every other level (`IPPROTO_TCP`, `IPPROTO_IP`, `IPPROTO_IPV6`) fails with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified From 72a685b906834098f97e916cfd7be70408f11a42 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 14:53:24 +0100 Subject: [PATCH 092/353] feat(libkernel): report _SC_CLK_TCK through sysconf The Python in PS5MC (PPSA99420) reads sysconf(_SC_CLK_TCK) while importing its posix module and failed with "cannot read ticks_per_second". It returns FreeBSD's CLK_TCK, 128. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/SystemConfiguration.cpp | 3 +++ core/libs/tests/GuestSystemConfiguration.cpp | 1 + docs/dev/debt/libkernel.md | 1 + 3 files changed, 5 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 672c9d2911..788d22c484 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -66,6 +66,9 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { const int saved = *__error_nid_postfix(); std::int64_t result = -1; switch (name) { + case 3: // _SC_CLK_TCK + result = 128; + break; case 47: // _SC_PAGESIZE result = PS5_PAGE_SIZE; break; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 1ad0f5c6f7..ea05abd3cd 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -262,6 +262,7 @@ int main() { CheckSdkVersionSysctl(); CheckArandomSysctl(); *__error_nid_postfix() = 13; + Require(sysconf_nid_postfix(3) == 128); Require(sysconf_nid_postfix(47) == 0x4000); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); Require(sysconf_nid_postfix(57) > 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 03abba3e13..6bc6b8d83a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -15,6 +15,7 @@ - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown +- [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified From 0d30059bf5ff30d45ca5d8c51d0f07f49558bd61 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 13:44:31 +0000 Subject: [PATCH 093/353] fix(libkernel): handle FIOCLEX and FIONCLEX in ioctl ioctl() on a file, directory, pipe or character device threw "request 0x20006601 ... is not implemented" for FIOCLEX, and the same for FIONCLEX, although the close-on-exec flag they change is already kept for these descriptors and reported by fcntl(F_GETFD). FreeBSD 11.4 handles both requests in kern_ioctl() for every descriptor, before the request reaches the file's own ioctl, by changing the flag F_GETFD reads. sys/kern/sys_generic.c, kern_ioctl(): case FIONCLEX: fdp->fd_ofiles[fd].fde_flags &= ~UF_EXCLOSE; goto out; case FIOCLEX: fdp->fd_ofiles[fd].fde_flags |= UF_EXCLOSE; goto out; sys/kern/kern_descrip.c, kern_fcntl(): case F_GETFD: ... td->td_retval[0] = (fde->fde_flags & UF_EXCLOSE) ? FD_CLOEXEC : 0; Both requests now set or clear that flag and return 0. libuv before 1.44 sets close-on-exec this way on FreeBSD (uv__cloexec_ioctl). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/DescriptorControl.cpp | 6 ++++++ core/libs/tests/GuestKernelFcntl.cpp | 10 ++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 5fa3171efb..1701669cf4 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -89,6 +89,8 @@ constexpr std::int16_t FromStart = 0; constexpr std::int16_t FromCurrent = 1; constexpr std::int16_t FromEnd = 2; +constexpr std::uint32_t CloseOnExecRequest = 0x20006601; +constexpr std::uint32_t KeepOnExecRequest = 0x20006602; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; constexpr std::uint32_t RequestIn = 0x80000000u; @@ -530,6 +532,10 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); const auto kind = Classify(fd, "ioctl"); if (!kind) return Fail(GuestEbadf); + if (command == CloseOnExecRequest || command == KeepOnExecRequest) { + SetCloseOnExec(fd, command == CloseOnExecRequest); + return 0; + } if (command == NonblockingRequest) { const bool enable = *static_cast(argument) != 0; return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index a839d3d034..679a739b64 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -63,6 +63,8 @@ static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; static constexpr std::uint64_t WindowSizeRequest = 0x40087468; static constexpr std::uint64_t AsyncRequest = 0x8004667d; +static constexpr std::uint64_t CloseOnExecRequest = 0x20006601; +static constexpr std::uint64_t KeepOnExecRequest = 0x20006602; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -123,6 +125,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(file, CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(file, KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); @@ -326,6 +332,10 @@ static void PipeFlagsRoundTrip() { Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(pipe[0], CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(pipe[1], KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 03abba3e13..c8793df610 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -155,7 +155,7 @@ - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. From f0da9edbfeb7dd7bc3674cf15846cde00396b9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Tue, 13 Oct 2026 11:05:22 -0300 Subject: [PATCH 094/353] fix(relinker): reject overflowing LSDA landing pad addresses --- .../UnusedNidFilter/EhFrameReader.cpp | 7 +++- .../relinker/tests/StrictNidFilterTests.cpp | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 8ce7c297b3..fd61157b6a 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -238,7 +238,12 @@ private: encoded(position, callEncoding); const auto landing = encoded(position, callEncoding); leb(position); - if (landing != 0) region.ExtraTargets.push_back(landingBase + landing); + if (landing != 0) { + const bool negative = (callEncoding & 0x08) != 0 && (landing >> 63) != 0; + if (negative ? landingBase < std::uint64_t{0} - landing : landing > std::numeric_limits::max() - landingBase) + throw RelinkerException("Strict filter: LSDA landing pad address overflowed", address); + region.ExtraTargets.push_back(landingBase + landing); + } } if (position != tableEnd) throw RelinkerException("Strict filter: LSDA call-site table size mismatch", address); } diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index bc31840e61..7fea13222a 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -271,6 +271,45 @@ void exceptionLandingPads() { emit(input, 31, {0xC3}); importThunk(input, 64, 0x2000); require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Exception-only import was removed"); + const auto checkLanding = [&](std::uint64_t base, std::uint64_t offset, bool signedOffset, bool rejected) { + auto data = bytes; + write(data, 0x280, 0); + write(data, 0x281, base); + write(data, 0x289, 0xFF); + write(data, 0x28A, signedOffset ? 12 : 4); + write(data, 0x28B, 25); + write(data, 0x28C, 0); + write(data, 0x294, 1); + write(data, 0x29C, offset); + write(data, 0x2A4, 0); + try { + const auto functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(data, headers, {}, {}); + require(!rejected, "Overflowing LSDA landing pad address was accepted"); + const auto expected = offset == 0 ? std::vector{0x1000} : + std::vector{0x1000, base + offset}; + require(functions.size() == 1 && functions[0].ExtraTargets == expected, + "Valid LSDA landing pad address changed"); + } catch (const Relinker::RelinkerException& error) { + require(rejected && std::string(error.what()).find("LSDA landing pad address overflowed") != std::string::npos, + "LSDA landing pad rejected for an unexpected reason"); + } + }; + const auto maximum = std::numeric_limits::max(); + for (const std::uint64_t base : {std::uint64_t{0x1000}, maximum - 16}) { + checkLanding(base, 0, false, false); + checkLanding(base, maximum - base, false, false); + checkLanding(base, maximum - base + 1, false, true); + checkLanding(base, maximum, false, true); + } + checkLanding(0x1000, maximum, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1000, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1001, true, true); + checkLanding(maximum - 16, 16, true, false); + checkLanding(maximum - 16, 17, true, true); + checkLanding(0, 0, true, false); + checkLanding(0, maximum, false, false); + checkLanding(std::uint64_t{1} << 63, std::uint64_t{1} << 63, true, false); + checkLanding((std::uint64_t{1} << 63) - 1, std::uint64_t{1} << 63, true, true); write(bytes, 0x282, 0xFF); requireFailure([&] { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); }, "Malformed LSDA was accepted"); } From 61890ae7acd5903dd71f297b53865edc71f1cff9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:43:20 -0700 Subject: [PATCH 095/353] fix(shader): capture image descriptors read from constant buffers at uniform offsets GetHandle refused every T# whose dwords are scalar buffer loads (ReadConstBuffer) before ValidateSource looked at them. Tables with a 32-byte stride and a wave-uniform key are planned as bindless tables before GetHandle runs, so the refusal only hit the other cases: a T# read at an offset the runtime walk can evaluate, like Resident Evil Village's mesh programs (48-byte records with the T# at +16, the record index read through two more constant buffers keyed by user data). ValidateSource already decides whether such a chain is a uniform runtime value, so leave the decision to it; the walk then reads the T# at each capture. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 58 +++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 9 --- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 46fc6edf7f..4d7fbbd0a5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -947,6 +947,63 @@ void verifyBindlessTable() { fillSrt(4u); } +void verifyUniformTableImage() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + struct alignas(4096) GuestTables { + std::array, 3> table{}; + std::array output{}; + std::array srt{}; + }; + static GuestTables guest; + const auto texture = [](const Texture& image) { + const auto base = static_cast(reinterpret_cast(image.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto first = texture(textures[0]); + const auto second = texture(textures[1]); + std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); + std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); + const auto bufferDescriptor = [](const void* base, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; + }; + const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); + const auto outputV = bufferDescriptor(guest.output.data(), 16u); + std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); + std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); + for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { + userData[2] = key; + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + } +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; @@ -3557,6 +3614,7 @@ int main(int argc, char** argv) { verifyPureFlatSlots(); verifyGuardedDescriptorSources(); verifyBindlessTable(); + verifyUniformTableImage(); verifyDescriptorPhis(); verifyLaneSelectedDescriptors(); verifyProgramCounterRelativeData(); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 675f7d561f..e2503445e8 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1156,15 +1156,6 @@ private: DescriptorSource descriptor; MakeSource(*handle, width, sampler, sampleAdjust, descriptor); std::uint32_t badDword = 0; - if (expected == IrOpcode::GetImageResource) { - for (; badDword < descriptor.dwordCount; badDword++) { - const IrValue* value2 = descriptor.dwords[badDword]->Resolve(); - if (value2->Opcode() == IrOpcode::ReadConstBuffer) { - fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); - } - } - badDword = 0; - } if (!ValidateSource(descriptor, badDword)) { fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); } From 34e7c309ad0959d4c4db28a6ffa3c6e21294503c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:43:50 -0700 Subject: [PATCH 096/353] fix(shader): read lane 63 of half-wave reductions whose keys exclude helper lanes On a 32-lane host, a wave64 pixel program's v_readlane_b32 of lane 63 is emitted only when its half-wave scan's keys are proven zero for lanes that did not enter. The proof walk accepted the not-helper test (IEqual32 of HelperInvocation and 0) only inside ballots, not as a lane bit, and it searched conjunctions depth first: a key mask like exec & ~vcc inside a loop spent the 512-step budget on the loop's exec phis before it reached the helper test. Accept the helper test as a lane bit, and look for a false or helper conjunct before walking an operand. Resident Evil Village's wave-wide OR reductions (DPP row shifts, v_permlanex16, v_readlane of lanes 31 and 63) failed this way. --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++++ .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 46fc6edf7f..2b0ba97714 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1370,6 +1370,10 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); + *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; + comparedScan.insert(comparedScan.begin(), 0x7c080080u); + require(reductions(pixel(comparedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by the lanes that failed a compare did not read lane 31 as a subgroup OR on 32 lanes"); for (const auto& reduction : scans) { const auto wide = "half-wave reduction: a wave64 scan reduced by SPIR-V opcode " + std::to_string(reduction.reduce) + " did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"; require(halves(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 64u, true), reduction.reduce, reduction.identity), wide.c_str()); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 7a4b02ea51..edb8d25e85 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -300,6 +300,18 @@ bool AllLanesBit(const IrValue* bit) { return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } +bool HelperTest(const IrValue* predicate) { + return Is(predicate, IrOpcode::IEqual32) && EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); +} + +bool HelperOrFalseConjunct(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit->Argument(0)) || HelperOrFalseConjunct(bit->Argument(1)); + return HelperTest(bit); +} + bool EveryInvocationEnters(IrShaderStage stage) { return stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation; } @@ -312,9 +324,10 @@ public: bit = Resolved(bit); if (bit == nullptr || !Spend()) return false; if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); + if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); const auto words = LaneBitWords(bit); return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); } @@ -332,7 +345,7 @@ private: if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); switch (predicate->Opcode()) { case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); + case IrOpcode::IEqual32: return HelperTest(predicate); default: return ZeroBit(predicate); } } From b6bc7218fd909faddff6f1d1bf769e03292716fa Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:23:14 -0700 Subject: [PATCH 097/353] test(shader): cover the search order of the half-wave key proof The proof that a half-wave scan's keys are zero for lanes that did not enter walks a conjunction depth first under a 512-step budget. Narrowing exec with 600 compares before the scan puts the helper-lane test at the bottom of a 600-deep LogicalAnd chain in the keys: walking the first operand first spends the whole budget before reaching it, and the v_readlane_b32 of lane 63 is refused. Looking for a false or helper conjunct before walking an operand finds it, so lane 31 becomes a subgroup OR on a 32-lane host. Resident Evil Village's wave64 pixel programs key these scans by a loop's exec ahead of the helper test. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2b0ba97714..816424f378 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1374,6 +1374,16 @@ void verifyHalfWaveReduction() { *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; comparedScan.insert(comparedScan.begin(), 0x7c080080u); require(reductions(pixel(comparedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by the lanes that failed a compare did not read lane 31 as a subgroup OR on 32 lanes"); + auto narrowedScan = comparedScan; + std::vector narrowing; + for (std::uint32_t step = 1; step <= 600u; ++step) { + const auto threshold = -static_cast(step); + std::uint32_t bits = 0; + std::memcpy(&bits, &threshold, sizeof(bits)); + narrowing.insert(narrowing.end(), {0x7c0800ffu, bits, 0x87fe6a7eu}); + } + narrowedScan.insert(narrowedScan.begin(), narrowing.begin(), narrowing.end()); + require(reductions(pixel(narrowedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by lanes that 600 compares narrowed exec to did not read lane 31 as a subgroup OR on 32 lanes"); for (const auto& reduction : scans) { const auto wide = "half-wave reduction: a wave64 scan reduced by SPIR-V opcode " + std::to_string(reduction.reduce) + " did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"; require(halves(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 64u, true), reduction.reduce, reduction.identity), wide.c_str()); From 6a46563a9357c3024dc59b79612db6692112e032 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:09:13 -0700 Subject: [PATCH 098/353] refactor(savedata): clear the mount result once the mount point is made #5173 changes the create condition right after the last error check, and #3950 removes the comment after the create block, so clearing the result in either place made the pull requests conflict. The result is still cleared only after every error return and before anything is written to it. --- core/libs/prx/libSceSaveData.native/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 719a79dbfe..8c455e7cbc 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -497,7 +497,6 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (slot == -1) { return SAVE_DATA_ERROR_MOUNT_FULL; } - std::memset(mount_result, 0, sizeof(*mount_result)); if (create || create2) { std::filesystem::create_directories(real_path); } @@ -505,6 +504,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result // path resolver maps it to the save directory, whose name may be far longer. const std::string mountPoint = "/savedata" + std::to_string(slot); AddPathAlias_nid_no_patch(mountPoint.c_str(), std::filesystem::absolute(real_path).string().c_str()); + std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; From fc8165e8b913b52f0f8a86cdcedad1cee832fba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Tue, 13 Oct 2026 16:14:27 +0200 Subject: [PATCH 099/353] test(cli): cover module-dir, sce-module-path and target option errors Add parser tests for --module-dir, --sce-module-path, --macos and --to-rosetta, including their repeat and conflict errors, which had no coverage. --- core/relinker/cli/tests/CliArgsTests.cpp | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 4331181592..305b66e719 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -224,6 +224,41 @@ void windowsFixedBase() { } +void moduleDirectories() { + const auto args = parse({"--module-dir", "Media/Plugins", "input.elf", "--module-dir", "extra", "output.elf"}); + require(args.moduleDirectories == std::vector{"Media/Plugins", "extra"}, "--module-dir directories were not recorded in order"); + require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "--module-dir consumed a positional path"); + expectThrows({"--module-dir"}, "--module-dir requires a directory"); + expectThrows({"--skip-sce-module", "--module-dir", "Media/Plugins", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); + expectThrows({"--module-dir", "Media/Plugins", "--skip-sce-module", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); +} + +void sceModulePath() { + const auto args = parse({"--sce-module-path", "modules", "input.elf", "output.elf"}); + require(args.sceModulePath == "modules", "--sce-module-path value was not captured"); + expectThrows({"--sce-module-path"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "--registry", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "a", "--sce-module-path", "b", "input.elf", "output.elf"}, "--sce-module-path must be specified once"); + expectThrows({"--skip-sce-module", "--sce-module-path", "modules", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); + expectThrows({"--sce-module-path", "modules", "--skip-sce-module", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); +} + +void targetSelection() { + const auto macos = parse({"--macos", "input.elf", "output.elf"}); + require(macos.toMacos && !macos.toWindows, "--macos did not select only the macOS target"); + const auto rosetta = parse({"--to-rosetta", "input.elf", "output.elf"}); + require(rosetta.toRosetta && rosetta.toIntel, "--to-rosetta did not imply --to-intel"); + const auto rosettaOnMacos = parse({"--macos", "--to-rosetta", "input.elf", "output.elf"}); + require(rosettaOnMacos.toMacos && rosettaOnMacos.toRosetta && rosettaOnMacos.toIntel, "--to-rosetta was rejected with --macos"); + const std::string targetConflict = "--windows conflicts with --macos"; + expectThrows({"--windows", "--macos", "input.elf", "output.elf"}, targetConflict); + expectThrows({"--macos", "--windows", "input.elf", "output.elf"}, targetConflict); + const std::string rosettaConflict = "--to-rosetta conflicts with --windows"; + expectThrows({"--windows", "--to-rosetta", "input.elf", "output.elf"}, rosettaConflict); + expectThrows({"--to-rosetta", "--windows", "input.elf", "output.elf"}, rosettaConflict); +} + int main() { try { defaults(); @@ -238,6 +273,9 @@ int main() { excludeSceModuleConsumesNextTokenVerbatim(); excludeSceModuleWithoutValue(); excludeConflictsWithSkip(); + moduleDirectories(); + sceModulePath(); + targetSelection(); executables(); unusedFilter(); unusedFilterRejectsBadValues(); From 0ba462f8e94ea91a7882b2d872d1864679e59fdd Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:27:18 +0200 Subject: [PATCH 100/353] fix(agc): skip an indexed draw with no indices or instances --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8a6d3cd77a..a7c9ea980d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1012,8 +1012,12 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P Require(draw.firstInstance <= std::numeric_limits::max() - (draw.instanceCount - 1u), "auto draw instance range overflow"); } } - if (args == nullptr) Require(draw.indexCount != 0 && draw.instanceCount != 0, "zero-count indexed draws are unsupported"); - else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + if (args == nullptr) { + if (draw.indexCount == 0 || draw.instanceCount == 0) { + inputs.nothing = true; + return inputs; + } + } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); inputs.indexBytes = static_cast(draw.indexCount) * draw.indexSize; const auto indexBytes = inputs.indexBytes; APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast(indexBytes)); From af19dbfabaf3ef666f55bb98d114bfc9aca88c39 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:27:18 +0200 Subject: [PATCH 101/353] test(agc): skip an indexed draw with no indices or instances --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 54e333d901..6073bfe324 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3983,6 +3983,9 @@ void rectListTests() { const auto complete = drawFailure(3, indexed); for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); } + Require(drawFailure(0, true).empty(), "an indexed draw without indices was not skipped"); + const AgcDriver::Pm4::DrawParameters noInstances{reinterpret_cast(indices.data()), 3, 2, 0, 0, true}; + Draw(context, state, noInstances, {}); } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { From f7fd7569fab73fd60e9ea948775a4578ba421ea9 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 12 Oct 2026 07:56:27 -0400 Subject: [PATCH 102/353] fix(agc): retire fully unmapped multisample targets --- .../Graphics/src/MultisampleTarget.cpp | 27 ++++++++-- .../prx/libSceAgcDriver/tests/Recorder.cpp | 53 +++++++++++++++++-- .../prx/libc/include/GuestAllocations.hpp | 1 + core/libs/prx/libc/src/GuestAllocations.cpp | 12 ++++- docs/dev/debt/libSceAgcDriver.md | 2 + 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 123627bbf2..de37327a5c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include #include #include @@ -153,6 +154,7 @@ public: } Context context; + std::shared_ptr> active = std::make_shared>(true); ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -175,6 +177,7 @@ struct GuardedRange { std::uint64_t address; std::uint64_t bytes; std::uint32_t samples; + std::shared_ptr> active; }; std::shared_mutex& guardedMutex() { @@ -196,7 +199,17 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } -void guard(const Context& context, const ColorTarget& target) { +void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { + if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; + range.active->store(false, std::memory_order_release); + return true; + }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); +} + +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -208,10 +221,13 @@ void guard(const Context& context, const ColorTarget& target) { bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples}); + guarded().push_back({context.device, target.address, bytes, target.samples, active}); anyGuarded().store(true, std::memory_order_release); static std::once_flag installed; - std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); + std::call_once(installed, [] { + GuestMemory::SetReadGuard(&guardReads); + }); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped); } std::mutex& targetsMutex() { @@ -226,14 +242,15 @@ std::vector>& targets() { MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; target->cmask.CheckAddress(use.cmaskAddress); target->ApplyFastClear(use); return *target; } targets().push_back(std::make_unique(context, use)); - guard(context, use); auto& created = *targets().back(); + guard(context, use, created.active); if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; @@ -380,6 +397,7 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; target->colorFill.NoteFill(address, bytes, pattern); const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); @@ -395,6 +413,7 @@ void ClearMultisampleTargets(VkDevice device) { std::unique_lock guardLock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); + if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); } void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 815cf01fcb..749b45ef55 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5055,6 +5055,12 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { context.nullDescriptors = true; TextureCache cache(context); context.textureCache = &cache; + const auto clearTargets = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + ClearMultisampleTargets(context.device); + }; const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { std::string message; try { @@ -5111,7 +5117,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { }, "a texture read", "a texture read"); refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); - ClearMultisampleTargets(context.device); + clearTargets(); target.tileMode = ColorTileMode::RenderTarget; { std::lock_guard gpu(GpuMutex()); @@ -5122,7 +5128,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target"); AgcDriver::GuestMemory::Read(address + targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) { target.extent = {128, 64}; target.surfaceExtent = target.extent; @@ -5136,7 +5142,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) { target.extent = {64, 32}; @@ -5151,9 +5157,48 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } AgcDriver::GuestMemory::Read(address + 256u, texels); + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.samples = 4; + VkImageView previous = VK_NULL_HANDLE; + for (const bool removeAllocation : {false, true}) { + VkImageView view = VK_NULL_HANDLE; + { + std::lock_guard gpu(GpuMutex()); + view = MultisampleTargetView(context, target); + Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + { + GuestAllocations::Mutation mutation; + mutation.Protect(block, bytes, true, true, true, [] {}); + } + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed"); + bool failed = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed, "the failed host unmap did not throw"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap"); + { + GuestAllocations::Mutation mutation; + if (removeAllocation) mutation.Remove(block); + else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {}); + mutation.Add(block, bytes, true, true, true); + } + RequireNoMultisampleTarget(address, bytes, "a texture read"); + AgcDriver::GuestMemory::Read(address + 256u, texels); + previous = view; + } + clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 231826bb92..ce42a0be61 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -83,6 +83,7 @@ void GuestAllocationsInvalidate_nid_postfix(std::uintptr_t address, std::size_t // until the lease is dropped by another thread. void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsSetGpuMapObserver_nid_postfix(void (*callback)(const Mapped& ranges, std::uint64_t generation)); +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsNoteGpuMapping_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 1faac83136..c7b286af87 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -49,6 +49,7 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; std::atomic gpuMapObserver{nullptr}; +std::atomic unmapObserver{nullptr}; std::atomic gpuMapObserverCalls{0}; std::chrono::milliseconds pinWait() { @@ -421,9 +422,10 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer) { const auto exact = ranges.find(range.address); if (exact != ranges.end() && exact->second->allocationBytes == exact->second->bytes) { ranges.erase(exact); - return; + } else { + std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); } - std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(range.address, range.bytes); } namespace { @@ -517,10 +519,16 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const bool last = !allocationRemains(range, patch); apply(reinterpret_cast(cursor), pieceEnd - cursor, reinterpret_cast(range.allocationAddress), last); applyPatch(patch); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(cursor, pieceEnd - cursor); any = true; cursor = pieceEnd; } require(any, "unmap address is not registered"); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(reinterpret_cast(pointer), bytes); +} + +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t, std::size_t)) { + unmapObserver.store(callback, std::memory_order_release); } Lease GuestAllocationsAcquire_nid_postfix() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 48d05c944b..52fc641310 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -27,6 +27,8 @@ ### Functional +- [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. + - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. From 36c11dc9266b59f194f97fcba58612708ff48be1 Mon Sep 17 00:00:00 2001 From: Zenodeon Date: Tue, 13 Oct 2026 18:20:23 +0400 Subject: [PATCH 103/353] fix(relinker): identify failed inputs and invalid dynamic values --- core/relinker/CMakeLists.txt | 3 + .../src/macos/MacOsGuestModuleWriter.cpp | 3 +- .../src/windows/WindowsGuestModuleWriter.cpp | 3 +- .../relinker/src/guest/GuestImageReader.cpp | 8 +- .../relinker/src/pipeline/Conversion.cpp | 12 ++- .../src/pipeline/RelinkerPipeline.cpp | 45 +++++----- .../relinker/tests/ConversionTests.cpp | 29 +++++++ .../tests/test_conversion_diagnostics.py | 83 +++++++++++++++++++ 8 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 core/relinker/relinker/tests/test_conversion_diagnostics.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2d92caeaba..f15f71fd8c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -387,6 +387,9 @@ if(BUILD_TESTING) set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) + add_test(NAME conversion_diagnostics COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_conversion_diagnostics.py $) + set_tests_properties(conversion_diagnostics PROPERTIES TIMEOUT 20) add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index ff7e31f79a..b8c3f7b06f 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -114,7 +115,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma try { return WriteMacOsImage(input); } catch (Domain::RelinkerException& error) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } } diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index bcc3469ddc..60a42e276e 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -84,7 +85,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI try { directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); } catch (Domain::RelinkerException& error) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } WindowsTrampolineBuilder().Build(guest.Trampolines, image, sections, nextRva); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index c07cb75358..f1734eeba5 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -87,8 +88,8 @@ std::vector GuestImageReader::ReadModuleNames(const std::vector bytes, const bool windows) const { - const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const try { + const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(Io::Utf8Path(path) + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) { std::ostringstream message; @@ -361,6 +362,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; +} catch (Domain::RelinkerException& error) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(path); + throw; } } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 5d6b038248..024ec4d0f5 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -34,7 +34,7 @@ namespace Relinker { Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {} -ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const { +ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const try { Domain::InputProgress inputProgress(progress, Io::Utf8Path(input.InputPath)); static_assert(std::endian::native == std::endian::little); if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system"); @@ -117,18 +117,16 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); std::vector executableBytes; - try { - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); - } catch (Domain::RelinkerException& error) { - error.InputPath = Io::Utf8Path(input.InputPath); - throw; - } + executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 1, 1}); std::string diagnostics; if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics)}; +} catch (Domain::RelinkerException& error) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); + throw; } } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2baf1b7130..2382160567 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -99,11 +99,13 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return false; }; - auto getTagValue = [&](const std::int64_t tag) -> std::uint64_t { + auto getTagValue = [&](const std::int64_t tag, const char* name) -> std::uint64_t { for (const auto& t : dynTags) if (t.Tag == tag) return t.Value; - throw RelinkerException("DT tag not found"); + std::ostringstream message; + message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; + throw RelinkerException(message.str()); }; auto requireExactlyOneOf = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) { @@ -111,24 +113,27 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const bool hasSysv = hasTag(sysvTag); if (hasOs && hasSysv) throw RelinkerException(std::string("Both DT_OS_ and DT_ variants present for ") + name); - if (!hasOs && !hasSysv) - throw RelinkerException(std::string("Neither DT_OS_ nor DT_ variant present for ") + name); + if (!hasOs && !hasSysv) { + std::ostringstream message; + message << "Neither DT_OS_ nor DT_ variant present for " << name << " (tags 0x" << std::hex << osTag << " and 0x" << sysvTag << ")"; + throw RelinkerException(message.str()); + } return hasOs; }; auto readAsOffset = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> FileByteOffset { if (requireExactlyOneOf(osTag, sysvTag, name)) { - const std::uint64_t value = getTagValue(osTag); + const std::uint64_t value = getTagValue(osTag, name); if (value > std::numeric_limits::max() - sceTableBase) throw RelinkerException(std::string("Adding PT_SCE_DYNLIBDATA base to ") + name + " overflows", value); return sceTableBase + value; } - return elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); + return elfReader->TranslateVirtualAddress(getTagValue(sysvTag, name)); }; auto readAsSize = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> ByteCount { requireExactlyOneOf(osTag, sysvTag, name); - return hasTag(osTag) ? getTagValue(osTag) : getTagValue(sysvTag); + return hasTag(osTag) ? getTagValue(osTag, name) : getTagValue(sysvTag, name); }; const bool hasPltRelocations = hasTag(DT_OS_PLTRELSZ) || hasTag(DT_PLTRELSZ) @@ -136,8 +141,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf || hasTag(DT_OS_JMPREL) || hasTag(DT_JMPREL); if (hasPltRelocations || hasTag(DT_OS_PLTGOT) || hasTag(DT_PLTGOT)) { gotVAddr = requireExactlyOneOf(DT_OS_PLTGOT, DT_PLTGOT, "DT_PLTGOT") - ? getTagValue(DT_OS_PLTGOT) - : getTagValue(DT_PLTGOT); + ? getTagValue(DT_OS_PLTGOT, "DT_OS_PLTGOT") + : getTagValue(DT_PLTGOT, "DT_PLTGOT"); } const FileByteOffset dynStrTabOffset = readAsOffset(DT_OS_STRTAB, DT_STRTAB, "DT_STRTAB"); @@ -145,30 +150,32 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const FileByteOffset dynSymTabOffset = readAsOffset(DT_OS_SYMTAB, DT_SYMTAB, "DT_SYMTAB"); constexpr std::size_t symEntSize = 24; - if (readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT") != symEntSize) - throw RelinkerException("Unsupported DT_SYMENT value"); + const auto symbolEntrySize = readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT"); + if (symbolEntrySize != symEntSize) + throw RelinkerException("Unsupported DT_SYMENT value: " + std::to_string(symbolEntrySize) + "; expected 24 bytes"); FileByteOffset dynJmpRelOffset = 0; if (hasPltRelocations) { gotSize = readAsSize(DT_OS_PLTRELSZ, DT_PLTRELSZ, "DT_PLTRELSZ"); const std::int64_t jmprelType = requireExactlyOneOf(DT_OS_PLTREL, DT_PLTREL, "DT_PLTREL") - ? getTagValue(DT_OS_PLTREL) - : getTagValue(DT_PLTREL); + ? getTagValue(DT_OS_PLTREL, "DT_OS_PLTREL") + : getTagValue(DT_PLTREL, "DT_PLTREL"); if (jmprelType != DT_RELA) - throw RelinkerException("Unsupported DT_PLTREL type"); + throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(jmprelType) + "; expected DT_RELA (7)"); dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); if (gotSize % 24 != 0) - throw RelinkerException("Invalid DT_PLTRELSZ value"); + throw RelinkerException("Invalid DT_PLTRELSZ value: " + std::to_string(gotSize) + " bytes; expected a multiple of 24 bytes"); } const ByteCount dynJmpRelSize = gotSize; const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); const ByteCount dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ"); constexpr std::size_t relaEntSize = 24; - if (readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT") != relaEntSize) - throw RelinkerException("Unsupported DT_RELAENT value"); + const auto relocationEntrySize = readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT"); + if (relocationEntrySize != relaEntSize) + throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(relocationEntrySize) + "; expected 24 bytes"); if (dynRelaSize % relaEntSize != 0) - throw RelinkerException("Invalid DT_RELASZ value"); + throw RelinkerException("Invalid DT_RELASZ value: " + std::to_string(dynRelaSize) + " bytes; expected a multiple of 24 bytes"); std::vector> neededLibraryNamesByStrOffset; for (const auto& tag : dynTags) @@ -384,7 +391,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } if (hasTag(DT_OS_SYMTABSZ)) { - const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); + const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ, "DT_OS_SYMTABSZ"); if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) throw RelinkerException("Invalid executable dynamic symbol table size"); for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index a2ff847e12..9e58d06fc7 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -162,6 +163,33 @@ void scanning() { require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); } +class FailingProgress final : public Domain::IProgress { +public: + std::string InputPath; + void Report(const Domain::ProgressEvent&) override { + Domain::RelinkerException error("diagnostic context", 0x1234); + error.InputPath = InputPath; + throw error; + } + void Message(std::string_view) override {} +}; + +void diagnosticContext() { + for (const bool specific : {false, true}) { + FailingProgress progress; + if (specific) progress.InputPath = "virtual/title/sce_module/plugin.prx"; + const auto source = input(); + try { Relinker::Conversion(progress).Convert(source, {}); } + catch (const Domain::RelinkerException& error) { + require(error.InputPath == (specific ? progress.InputPath : Io::Utf8Path(source.InputPath)), "Wrong diagnostic input path"); + require(error.FailureOffset == 0x1234, "Diagnostic offset changed"); + require(std::string(error.what()) == "diagnostic context", "Diagnostic message changed"); + continue; + } + throw std::runtime_error("Diagnostic failure was swallowed"); + } +} + class CancellingProgress final : public Domain::IProgress { public: Domain::ProgressStage Stage = Domain::ProgressStage::Decode; @@ -192,6 +220,7 @@ int main() { try { convert(); scanning(); + diagnosticContext(); cancellation(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/test_conversion_diagnostics.py b/core/relinker/relinker/tests/test_conversion_diagnostics.py new file mode 100644 index 0000000000..776206cc5f --- /dev/null +++ b/core/relinker/relinker/tests/test_conversion_diagnostics.py @@ -0,0 +1,83 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture +from test_guest_intel_trampolines import guest_fixture, main_fixture, PLAIN_SITE + + +def main(): + relinker = Path(sys.argv[1]).resolve() + failures = [] + with tempfile.TemporaryDirectory(prefix="anyps5-diagnostics-") as directory: + work = Path(directory) / "title space \u00e9" + work.mkdir() + + def convert(name, image, expected, flags, module=None, extra=False): + case = work / name + case.mkdir() + source = case / "eboot.bin" + source.write_bytes(main_fixture() if module is not None or extra else image) + options = [] + offending = source + if module is not None: + modules = case / "sce_module" + modules.mkdir() + offending = modules / "broken \u00e9.prx" + offending.write_bytes(module) + options = ["--sce-module-path", str(case)] + elif extra: + offending = case / "picker.bin" + offending.write_bytes(image) + (case / "sce_module").mkdir() + options = ["--sce-module-path", str(case), "--executable", "picker.bin"] + else: + options = ["--skip-sce-module"] + output = case / "converted.out" + result = subprocess.run([str(relinker), *flags, *options, str(source), str(output)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + valid = (result.returncode == 2 and expected in result.stderr + and result.stderr.splitlines().count(f"Input: {offending}") == 1 + and not output.exists() and not list(case.rglob("*.guest.prx"))) + if extra: + valid = valid and not (case / "picker.bin.out").exists() + if not valid: + failures.append((name, result.returncode, result.stdout, result.stderr)) + + cases = [] + for tag, value, message in ( + (11, 16, "Unsupported DT_SYMENT value: 16; expected 24 bytes"), + (9, 8, "Unsupported DT_RELAENT value: 8; expected 24 bytes"), + (8, 25, "Invalid DT_RELASZ value: 25 bytes; expected a multiple of 24 bytes"), + ): + image = fixture() + for offset in range(0x400, 0x470, 16): + if struct.unpack_from(" Date: Tue, 13 Oct 2026 18:37:50 +0400 Subject: [PATCH 104/353] fix(relinker): keep diagnostics compatible with pending refactors --- .../relinker/src/pipeline/Conversion.cpp | 7 ++- .../src/pipeline/RelinkerPipeline.cpp | 44 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 024ec4d0f5..2b69cfcd12 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -117,7 +117,12 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); std::vector executableBytes; - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); + try { + executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); + } catch (Domain::RelinkerException& error) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); + throw; + } if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 1, 1}); std::string diagnostics; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2382160567..4ccaba99fa 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -99,10 +99,36 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return false; }; - auto getTagValue = [&](const std::int64_t tag, const char* name) -> std::uint64_t { + auto getTagValue = [&](const std::int64_t tag) -> std::uint64_t { for (const auto& t : dynTags) if (t.Tag == tag) return t.Value; + const char* name = "unknown"; + switch (tag) { + case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break; + case DT_STRTAB: name = "DT_STRTAB"; break; + case DT_OS_STRSZ: name = "DT_OS_STRSZ"; break; + case DT_STRSZ: name = "DT_STRSZ"; break; + case DT_OS_SYMTAB: name = "DT_OS_SYMTAB"; break; + case DT_SYMTAB: name = "DT_SYMTAB"; break; + case DT_OS_SYMENT: name = "DT_OS_SYMENT"; break; + case DT_SYMENT: name = "DT_SYMENT"; break; + case DT_OS_PLTGOT: name = "DT_OS_PLTGOT"; break; + case DT_PLTGOT: name = "DT_PLTGOT"; break; + case DT_OS_PLTREL: name = "DT_OS_PLTREL"; break; + case DT_PLTREL: name = "DT_PLTREL"; break; + case DT_OS_PLTRELSZ: name = "DT_OS_PLTRELSZ"; break; + case DT_PLTRELSZ: name = "DT_PLTRELSZ"; break; + case DT_OS_JMPREL: name = "DT_OS_JMPREL"; break; + case DT_JMPREL: name = "DT_JMPREL"; break; + case DT_OS_RELA: name = "DT_OS_RELA"; break; + case DT_RELA: name = "DT_RELA"; break; + case DT_OS_RELASZ: name = "DT_OS_RELASZ"; break; + case DT_RELASZ: name = "DT_RELASZ"; break; + case DT_OS_RELAENT: name = "DT_OS_RELAENT"; break; + case DT_RELAENT: name = "DT_RELAENT"; break; + case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break; + } std::ostringstream message; message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; throw RelinkerException(message.str()); @@ -123,17 +149,17 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf auto readAsOffset = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> FileByteOffset { if (requireExactlyOneOf(osTag, sysvTag, name)) { - const std::uint64_t value = getTagValue(osTag, name); + const std::uint64_t value = getTagValue(osTag); if (value > std::numeric_limits::max() - sceTableBase) throw RelinkerException(std::string("Adding PT_SCE_DYNLIBDATA base to ") + name + " overflows", value); return sceTableBase + value; } - return elfReader->TranslateVirtualAddress(getTagValue(sysvTag, name)); + return elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); }; auto readAsSize = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> ByteCount { requireExactlyOneOf(osTag, sysvTag, name); - return hasTag(osTag) ? getTagValue(osTag, name) : getTagValue(sysvTag, name); + return hasTag(osTag) ? getTagValue(osTag) : getTagValue(sysvTag); }; const bool hasPltRelocations = hasTag(DT_OS_PLTRELSZ) || hasTag(DT_PLTRELSZ) @@ -141,8 +167,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf || hasTag(DT_OS_JMPREL) || hasTag(DT_JMPREL); if (hasPltRelocations || hasTag(DT_OS_PLTGOT) || hasTag(DT_PLTGOT)) { gotVAddr = requireExactlyOneOf(DT_OS_PLTGOT, DT_PLTGOT, "DT_PLTGOT") - ? getTagValue(DT_OS_PLTGOT, "DT_OS_PLTGOT") - : getTagValue(DT_PLTGOT, "DT_PLTGOT"); + ? getTagValue(DT_OS_PLTGOT) + : getTagValue(DT_PLTGOT); } const FileByteOffset dynStrTabOffset = readAsOffset(DT_OS_STRTAB, DT_STRTAB, "DT_STRTAB"); @@ -158,8 +184,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (hasPltRelocations) { gotSize = readAsSize(DT_OS_PLTRELSZ, DT_PLTRELSZ, "DT_PLTRELSZ"); const std::int64_t jmprelType = requireExactlyOneOf(DT_OS_PLTREL, DT_PLTREL, "DT_PLTREL") - ? getTagValue(DT_OS_PLTREL, "DT_OS_PLTREL") - : getTagValue(DT_PLTREL, "DT_PLTREL"); + ? getTagValue(DT_OS_PLTREL) + : getTagValue(DT_PLTREL); if (jmprelType != DT_RELA) throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(jmprelType) + "; expected DT_RELA (7)"); dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); @@ -391,7 +417,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } if (hasTag(DT_OS_SYMTABSZ)) { - const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ, "DT_OS_SYMTABSZ"); + const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) throw RelinkerException("Invalid executable dynamic symbol table size"); for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { From 417ea99df56432497b4bd0d14248838889dc2a79 Mon Sep 17 00:00:00 2001 From: Nesyth <18372752+Nesyth@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:00:00 +0200 Subject: [PATCH 105/353] fix(libkernel): zero-fill APR reads that reach the end of the file --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 3 +- core/libs/tests/GuestAmpr.cpp | 66 +++++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 57199523e2..22e9358fc2 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -168,7 +168,8 @@ void _readFile(const Apr::ReadFileCommand& command) { stream.read(reinterpret_cast(command.destination), static_cast(command.size)); if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); const auto read = static_cast(stream.gcount()); - if (read != command.size) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); + if (read != command.size && (!stream.eof() || command.offset + read < std::filesystem::file_size(file.path))) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); + if (read != command.size) std::memset(reinterpret_cast(command.destination) + read, 0, static_cast(command.size - read)); } void _writeAddress(const Apr::WriteAddressCommand& command) { diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2845913f5c..e7e80e9e27 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -651,6 +651,70 @@ void TestZeroFilledBuffer() { std::remove(path); } +void TestReadPastEndOfFileZeroFills() { + const std::filesystem::path directory = "ampr_read_past_end"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + const auto file = directory / "settings.ini"; + { + std::ofstream stream(file, std::ios::binary); + for (std::size_t offset = 0; offset < 64; ++offset) stream.put(static_cast(FileByte(offset))); + } + const std::string filePath = file.generic_string(); + const char* path = filePath.c_str(); + std::uint32_t fileId = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &fileId, &size, &failed) == 0 && size == 64); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + for (std::size_t offset = 1; offset < size; ++offset) stream.put(static_cast(FileByte(offset))); + } + + std::array whole; + std::array continued; + std::array atEnd; + std::array pastEnd; + whole.fill(0xA5); + continued.fill(0xA5); + atEnd.fill(0xA5); + pastEnd.fill(0xA5); + Recorder recorder; + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, whole.data(), size, 0) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, continued.data(), 16) == 0); + Require(sceAmprAprCommandBufferReadFileGather(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, 8, 0) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, atEnd.data(), atEnd.size(), size - 1) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, pastEnd.data(), pastEnd.size(), 4096) == 0); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(MatchesFile(whole.data(), 1, size - 1) && whole[size - 1] == 0); + for (std::size_t index = 0; index < 16; ++index) Require(continued[index] == 0); + Require(MatchesFile(continued.data() + 16, 1, 8)); + for (const auto byte : atEnd) Require(byte == 0); + for (const auto byte : pastEnd) Require(byte == 0); + std::filesystem::remove_all(directory); +} + +void TestReadAtUnseekableOffsetThrows() { + const char* path = "ampr_unseekable_offset.bin"; + { std::ofstream file(path, std::ios::binary); file << "data"; } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + std::array data{}; + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, fileId, 0, reinterpret_cast(data.data()), data.size(), std::uint64_t{1} << 63u}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("returned 0 of 4 bytes") != std::string::npos; + } + Require(rejected); + std::remove(path); +} + void TestResolveFailures() { const char* paths[]{"ampr_resolve_existing.bin", "ampr_resolve_missing.bin"}; { std::ofstream file(paths[0], std::ios::binary); file << "data"; } @@ -1403,6 +1467,8 @@ int main() { TestClearBuffer(); TestConstructed(); TestZeroFilledBuffer(); + TestReadPastEndOfFileZeroFills(); + TestReadAtUnseekableOffsetThrows(); TestResolveFailures(); TestMalformedCommandSizes(); TestResolveSymbolicLink(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e5441d53ec..bf86d7d405 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -22,6 +22,7 @@ - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` +- [APR reads past the end of a file](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - a read that reaches the current end of the file on disk copies the bytes the file has and fills the rest of the destination with zeros; a read that comes back short before that end still throws. PPSA18605 resolves its settings file once, rewrites it one byte shorter and reads it again with the size that resolve returned. The console's behaviour for a read past the end of a file is unverified: it may zero-fill, leave the destination untouched or fail the command - [AMPR counter access](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output From 78f620cf9e2408fc7a314569c05a3f99f3e05d98 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Tue, 13 Oct 2026 19:26:57 +0530 Subject: [PATCH 106/353] fix(relinker): keep code analysis inside the code extent of PS4 executables Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 1 + .../relinker/analysis/SceCodeExtent.hpp | 14 +++++++ .../src/analysis/CodeInstructionCollector.cpp | 25 ++++++++++-- .../relinker/src/analysis/SceCodeExtent.cpp | 40 +++++++++++++++++++ .../src/pipeline/RelinkerPipeline.cpp | 35 ++-------------- .../relinker/tests/test_sce_code_extent.py | 35 +++++++++++++--- docs/dev/debt/relinker.md | 1 + 7 files changed, 110 insertions(+), 41 deletions(-) create mode 100644 core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp create mode 100644 core/relinker/relinker/src/analysis/SceCodeExtent.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2d92caeaba..24cc93879a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -54,6 +54,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SceCodeExtent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp new file mode 100644 index 0000000000..c43fd18cd6 --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -0,0 +1,14 @@ +#ifndef RELINKER_ANALYSIS_SCECODEEXTENT_HPP +#define RELINKER_ANALYSIS_SCECODEEXTENT_HPP + +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); + +} + +#endif diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 2fd1a9a2eb..c5ffda7b73 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -53,8 +54,9 @@ std::set CodeInstructionCollector::Collect(const std::ve } throw Domain::RelinkerException("Code analysis: unmapped address", address); }; + auto codeHeaders = headers; const auto isCode = [&](std::uint64_t address) { - for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; + for (const auto& header : codeHeaders) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; return false; }; std::map tags; @@ -91,6 +93,21 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + if (sceBase) { + const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + for (auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + range(header.Offset, header.FileSize); + const std::span segment(bytes.data() + header.Offset, header.FileSize); + const auto end = FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize); + 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); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + } + } std::set roots; std::map functions; const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; @@ -344,11 +361,11 @@ std::set CodeInstructionCollector::Collect(const std::ve } std::size_t previousRoots = 0; std::unordered_set decodedInstructions; - std::vector> sectionReachable(headers.size()); + std::vector> sectionReachable(codeHeaders.size()); do { previousRoots = roots.size(); - for (std::size_t i = 0; i < headers.size(); ++i) { - const auto& header = headers[i]; + for (std::size_t i = 0; i < codeHeaders.size(); ++i) { + const auto& header = codeHeaders[i]; if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize == 0) continue; range(header.Offset, header.FileSize); std::vector entries; diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp new file mode 100644 index 0000000000..106528e542 --- /dev/null +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -0,0 +1,40 @@ +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize) { + VirtualAddress codeEnd = 0; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) + codeEnd = std::max(codeEnd, function.End); + if (codeEnd == 0) + return std::nullopt; + if (codeEnd - segmentVAddr > segment.size()) + throw RelinkerException("Unwind function exceeds executable segment", codeEnd); + if (pltRelocationsSize == 0) + return codeEnd; + const auto target = [&](const std::size_t at) { + std::int32_t displacement = 0; + std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); + return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); + }; + const std::size_t lastFunctionEnd = codeEnd - segmentVAddr; + std::optional plt; + for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { + if (segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 + && target(at) == pltGot + 8 && target(at + 6) == pltGot + 16) { + plt = at; + break; + } + } + if (!plt) + throw RelinkerException("PLT not found after the last unwind function", codeEnd); + const ByteCount pltSize = 16 * (1 + pltRelocationsSize / 24); + if (pltSize > segment.size() - *plt) + throw RelinkerException("PLT exceeds executable segment", segmentVAddr + *plt); + return segmentVAddr + *plt + pltSize; +} + +} diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2baf1b7130..0b93b5031d 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -3,8 +3,8 @@ #include #include #include +#include #include -#include #include #include @@ -278,37 +278,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { static_cast(segmentFileOffset); - VirtualAddress codeEnd = 0; - for (const auto& function : functions) - if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) - codeEnd = std::max(codeEnd, function.End); - if (codeEnd == 0) - continue; - if (codeEnd - segmentVAddr > segment.size()) - throw RelinkerException("Unwind function exceeds executable segment", codeEnd); - if (hasPltRelocations && dynJmpRelSize != 0) { - const auto target = [&](const std::size_t at) { - std::int32_t displacement = 0; - std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); - return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); - }; - const std::size_t lastFunctionEnd = codeEnd - segmentVAddr; - std::optional plt; - for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { - if (segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 - && target(at) == gotVAddr + 8 && target(at + 6) == gotVAddr + 16) { - plt = at; - break; - } - } - if (!plt) - throw RelinkerException("PLT not found after the last unwind function", codeEnd); - const ByteCount pltSize = 16 * (1 + dynJmpRelSize / relaEntSize); - if (pltSize > segment.size() - *plt) - throw RelinkerException("PLT exceeds executable segment", segmentVAddr + *plt); - codeEnd = segmentVAddr + *plt + pltSize; - } - segment.resize(codeEnd - segmentVAddr); + if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) + segment.resize(*codeEnd - segmentVAddr); } } diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 8abcbc6e30..4cc8f11044 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -7,6 +7,7 @@ import tempfile PT_LOAD = 1 PT_DYNAMIC = 2 +PT_INTERP = 3 PT_GNU_EH_FRAME = 0x6474E550 PT_SCE_DYNLIBDATA = 0x61000000 PT_SCE_VERSION = 0x6FFFFF01 @@ -15,19 +16,26 @@ FUNCTION_SIZE = 0x10 GOT = 0xA00 EH_FRAME_HEADER = 0x800 EH_FRAME = 0x840 +POINTER = 0xB00 +INTERPRETER = b"/libexec/ld-elf.so.1\0" +OVERLAPPING = bytes.fromhex("b8 64 c3 90 90 72 fa c3") +UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") -def fixture(sce_block=True, plt_push_offset=8, gap=0): +def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False): plt = FUNCTION + FUNCTION_SIZE + gap + entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 + interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" Date: Tue, 13 Oct 2026 15:36:47 +0200 Subject: [PATCH 107/353] fix(libSceKeyboard): report one key slot when no key is held A keyboard record on the console never counts fewer than one key: with nothing held, and with no keyboard connected, the count is 1 and that slot holds code 0. SharpProspero's keyboard reader, PS5_Quake3e's notes on records read from a console and the PS5 input research notes all say so. AnyPS5 reported a count of 0. The count is now at least 1; the key slots themselves are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp | 2 +- core/libs/prx/libSceKeyboard/tests/Keyboard.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 2bb6273e75..0a16e2d75e 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -29,7 +29,7 @@ KeyboardData snapshot() { KeyboardData data{}; data.timestamp = sceKernelGetProcessTime(); data.connected = connected; - data.length = keyCount; + data.length = std::max(keyCount, 1); data.led = led; data.modifier_key = modifiers; std::copy_n(keys.begin(), keyCount, data.key_code); diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 802e1da750..cb05958e16 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -54,7 +54,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardReadState(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); KeyboardInput input; @@ -66,7 +66,7 @@ int main() { input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_RETURN, KMOD_NUM | KMOD_LSHIFT), 7); input.HandleEvent(KeyEvent(SDL_KEYUP, SDL_SCANCODE_A, KMOD_NUM | KMOD_LSHIFT), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 16) == 4); - Require(data[0].length == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); + Require(data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); Require(data[1].length == 1 && data[1].key_code[0] == 0x04); Require(data[2].length == 2 && data[2].key_code[0] == 0x04 && data[2].key_code[1] == 0x28); Require(data[3].length == 1 && data[3].key_code[0] == 0x28 && data[3].modifier_key == 2); @@ -84,7 +84,7 @@ int main() { window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_B), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; @@ -92,7 +92,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); @@ -142,6 +142,6 @@ int main() { Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 0); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); } From fb41f1082bbcba3a7ca961120e4b6f8af512702a Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:03:29 +0200 Subject: [PATCH 108/353] fix(libSceOpusDec): decode narrowband SILK packets FFmpeg resamples narrowband SILK (TOC configurations 0-3) from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth: a 20 ms packet decodes to 936 samples and the next wider packet to 984. sceOpusDecDecode required exactly the packet's duration and threw, so ProsperoRadio aborted on a narrowband Opus station. The decoder now keeps FFmpeg's output in a queue and returns exactly the packet's duration, as libopus does: the first narrowband packet starts with 24 samples of silence and the held-back samples follow in the next calls. The new test decodes three libopus 6 kb/s narrowband packets and a wideband packet after them; it aborts on main. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceOpusDec/Export.cpp | 17 +++++++++++----- core/libs/tests/GuestOpusDec.cpp | 27 ++++++++++++++++++++++++++ docs/dev/debt/libSceOpusDec.md | 1 + 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index 3e1729acb6..383c6e3117 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -8,6 +8,7 @@ #include #include #include +#include extern "C" { #include @@ -19,6 +20,7 @@ struct Decoder { std::uint32_t context; int channels; AVCodecContext* codec = nullptr; + std::vector pending; ~Decoder() { avcodec_free_context(&codec); } }; @@ -115,17 +117,22 @@ int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) throw std::runtime_error("OpusDec: FFmpeg rejected the packet"); const auto format = static_cast(frame->format); - if (frame->nb_samples != samples || frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || + if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) throw std::runtime_error("OpusDec: unexpected FFmpeg PCM format"); - for (int sample = 0; sample < samples; ++sample) { + for (int sample = 0; sample < frame->nb_samples; ++sample) { for (int channel = 0; channel < decoder.channels; ++channel) { const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - const float value = source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]; - const auto converted = static_cast(std::lrint(std::clamp(value * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast(pcm) + (sample * decoder.channels + channel) * 2, &converted, 2); + decoder.pending.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); } } + const auto count = static_cast(samples) * decoder.channels; + if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); + for (std::size_t index = 0; index < count; ++index) { + const auto converted = static_cast(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); + std::memcpy(reinterpret_cast(pcm) + index * 2, &converted, 2); + } + decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast(count)); return outputBytes; } diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index ef1bf7304d..0b462ff31c 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -40,6 +40,12 @@ const std::uint8_t OPUS_STEREO[] = { 0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5, }; +const std::uint8_t OPUS_NARROWBAND[] = { + 11, 0, 0x08, 0x83, 0x9C, 0xE1, 0x7B, 0x2F, 0x52, 0xCA, 0x9E, 0x75, 0xD8, + 13, 0, 0x08, 0x96, 0x51, 0x3A, 0x33, 0x4A, 0xF8, 0x29, 0xF5, 0xBE, 0x40, 0x15, 0x98, + 10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48, +}; + } // namespace int main() { @@ -111,4 +117,25 @@ int main() { Require(sceOpusDecDecode(otherState.data(), OPUS_STEREO + offset + 2, OPUS_STEREO[offset], reference.data(), 3840) == 3840); Require(sceOpusDecDestroy(otherState.data()) == 0); Require(sceOpusDecTerminate(&otherContext) == 0); + + std::uint32_t narrowContext = 0; + std::array narrowState{}; + Require(sceOpusDecInitialize(&narrowContext) == 0); + Require(sceOpusDecCreateEx(&narrowContext, narrowState.data(), 48000, 1) == 0); + std::array narrow{}; + bool audible = false; + for (std::size_t at = 0; at < sizeof(OPUS_NARROWBAND);) { + const int bytes = OPUS_NARROWBAND[at] | (OPUS_NARROWBAND[at + 1] << 8); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_NARROWBAND + at + 2, bytes, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + audible = audible || std::any_of(narrow.begin() + 1, narrow.end() - 1, [](auto value) { return value != 0 && value != 12345; }); + at += bytes + 2; + } + Require(audible); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_STEREO + 2, 70, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + Require(sceOpusDecDestroy(narrowState.data()) == 0); + Require(sceOpusDecTerminate(&narrowContext) == 0); } diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 535f40cfa7..74c417e67e 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -3,3 +3,4 @@ ### Functional - [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap From 0adbdcf45c059bd271a475d87f296a3dc8f95b72 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 01:45:31 +1300 Subject: [PATCH 109/353] perf(agc): cache a draw key the second time it is seen A draw whose key has no entry captured its stages, built a variant per stage (a copy of every captured region) and inserted a draw entry. A key that never comes back paid for the variants and the entry for nothing, and its entry pushed a live one out of the cache. Draws whose user data changes every frame are such keys. The driver now remembers the keys it has seen once, in a table of four-way buckets holding four times the cache's size in keys. A key gets its entry at its second sight; a key that already has an entry still takes new variants at once. A full bucket replaces its oldest key, so keys that share a bucket do not push each other out until more than four of them come between two sights of one. Test: agc_driver_draw_cache_reuse draws three times. The first two draws find no entry, one entry is inserted, and the third draw hits it. A sighting table of one bucket keeps two keys drawn in turn and replaces its oldest key when full. --- .../include/Driver/Draw/DrawCache.hpp | 13 ++++++++ .../Execution/include/Driver/Driver.hpp | 2 ++ .../Execution/src/Driver/Draw/DrawCache.cpp | 31 +++++++++++++++++++ .../Execution/src/Driver/Draw/DrawCapture.cpp | 2 +- .../tests/execution/DrawCacheReuse.cpp | 24 +++++++++++--- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 5fa2834621..859344a126 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -75,6 +75,19 @@ struct DrawEntryCounters { std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +class DrawKeySightings { +public: + static constexpr std::size_t Ways = 4; + explicit DrawKeySightings(std::size_t keys); + bool SeenBefore(std::uint64_t key); + +private: + std::vector keys; + std::vector filled; + std::vector next; + std::size_t bucketMask = 0; +}; + enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ce483328b3..c305a1c933 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -115,6 +115,7 @@ private: static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode); + bool admitDrawKey(std::uint64_t key); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected); std::shared_ptr findDrawRecipe(std::uint64_t key, const std::vector>& stages); void attachDrawRecipe(std::uint64_t key, const std::vector>& stages, std::shared_ptr recipe); @@ -310,6 +311,7 @@ private: std::list drawOrder; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; + std::optional drawKeySightings; DrawEntryCounters drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 7844679fd2..31803fd0f9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include +#include #include namespace AgcDriver::DriverDetail { @@ -56,6 +57,36 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) { } } +DrawKeySightings::DrawKeySightings(std::size_t keyCount) { + Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket"); + const auto buckets = std::bit_ceil(keyCount / Ways); + keys.assign(buckets * Ways, 0); + filled.assign(buckets, 0); + next.assign(buckets, 0); + bucketMask = buckets - 1; +} + +bool DrawKeySightings::SeenBefore(std::uint64_t key) { + auto mixed = key; + mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull; + mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull; + mixed ^= mixed >> 31u; + const auto bucket = static_cast(mixed) & bucketMask; + auto* ways = keys.data() + bucket * Ways; + if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true; + ways[next[bucket]] = key; + next[bucket] = static_cast((next[bucket] + 1u) % Ways); + if (filled[bucket] < Ways) ++filled[bucket]; + return false; +} + +bool Driver::admitDrawKey(std::uint64_t key) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawCache.contains(key)) return true; + if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); + return drawKeySightings->SeenBefore(key); +} + void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 3c6931fd2e..b4f9d0cd50 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st } void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) { + if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) { phaseTiming.Phase(DrawRowVectors); std::uint64_t unstable = 0, mismatches = 0; for (std::size_t i = 0; i < programs.size(); ++i) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 93c8237c2e..23fbd3392b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -18,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using AgcDriver::DriverDetail::DrawMiss; +using AgcDriver::DriverDetail::DrawKeySightings; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; @@ -48,7 +49,7 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawTwice() { +std::vector DrawThrice() { const auto color = reinterpret_cast(ColorMemory.data()); const auto vertex = reinterpret_cast(VertexCode.data()); const auto pixel = reinterpret_cast(PixelCode.data()); @@ -83,28 +84,41 @@ std::vector DrawTwice() { for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); Append(commands, 0x2d, {3, 2}); Append(commands, 0x2d, {3, 2}); + Append(commands, 0x2d, {3, 2}); return commands; } -void SecondDrawHitsTheCache(bool libraries) { +void ThirdDrawHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - auto commands = DrawTwice(); + auto commands = DrawThrice(); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); AgcDriverWaitIdle_nid_postfix(); const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; const auto layout = counters.misses[static_cast(DrawMiss::Layout)]; - Require(counters.lookups == 2 && counters.absent == 1, mode + ": the two draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 2 lookups with 1 absent"); + Require(counters.lookups == 3 && counters.absent == 2, mode + ": the three draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 3 lookups with 2 absent"); + Require(counters.inserts == 1, mode + ": the three draws inserted " + std::to_string(counters.inserts) + " draw entries, expected 1: a key is cached the second time it is seen"); Require(layout == 0, mode + ": the repeated draw missed the draw cache on its push offset (" + std::to_string(layout) + " layout misses)"); Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void SightingsKeepCollidingKeys() { + DrawKeySightings sightings(DrawKeySightings::Ways); + Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); + Require(!sightings.SeenBefore(11) && !sightings.SeenBefore(12), "an unseen draw key was admitted"); + Require(sightings.SeenBefore(11) && sightings.SeenBefore(12) && sightings.SeenBefore(11), "two keys in one bucket, drawn in turn, were not admitted at their second sight"); + Require(!sightings.SeenBefore(13) && !sightings.SeenBefore(14), "an unseen draw key was admitted"); + Require(!sightings.SeenBefore(0), "the oldest key of a full bucket was kept over the newest"); + Require(sightings.SeenBefore(12) && sightings.SeenBefore(13) && sightings.SeenBefore(14), "a full bucket lost a key newer than the one it replaced"); +} + } int main() { try { + SightingsKeepCollidingKeys(); bool libraries = false; { const auto device = OpenVulkanTestDevice(); @@ -112,7 +126,7 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; - SecondDrawHitsTheCache(libraries); + ThirdDrawHitsTheCache(libraries); AgcDriverShutdown_nid_postfix(); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; From 38bc0fd3488263551d7f69aee956afad7b9252e3 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:09:03 +0200 Subject: [PATCH 110/353] feat(input): turn mouse speed into a smooth right-stick tilt Co-Authored-By: Claude Opus 5.5 --- .../prx/libScePad/include/InputMapping.hpp | 7 +- .../prx/libScePad/include/KeyboardMouse.hpp | 70 ++++ core/libs/prx/libSceVideoOut/CMakeLists.txt | 12 + .../prx/libSceVideoOut/include/PadInput.hpp | 12 +- .../prx/libSceVideoOut/src/InputMapping.cpp | 38 ++- .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 193 +++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 146 ++------ .../prx/libSceVideoOut/tests/InputMapping.cpp | 33 ++ .../libSceVideoOut/tests/KeyboardMouse.cpp | 318 ++++++++++++++++++ .../libSceVideoOut/tests/PadSimulation.hpp | 129 +++++++ docs/user/INPUT_MAPPING.md | 24 +- 11 files changed, 843 insertions(+), 139 deletions(-) create mode 100644 core/libs/prx/libScePad/include/KeyboardMouse.hpp create mode 100644 core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index 21cccc0aed..a8914303d5 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -11,7 +11,6 @@ namespace Pad { -inline constexpr int MousePollIntervalMs = 33; inline constexpr int WheelPressDurationMs = 80; inline constexpr double DefaultMouseSensitivity = 1.0; inline constexpr double MaxMouseSensitivity = 10.0; @@ -21,6 +20,12 @@ struct InputSettings { int leftStickDeadzonePercent = 0; int rightStickDeadzonePercent = 0; double mouseSensitivity = DefaultMouseSensitivity; + double mouseVerticalScale = 1.0; + double mouseCurve = 1.0; + double mouseDeadzoneOffset = 0.2; + double mouseMaxSpeed = 1.0; + double mouseSmoothingMs = 10.0; + double mouseDecayMs = 30.0; }; struct InputConfiguration { diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp new file mode 100644 index 0000000000..68e501a384 --- /dev/null +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP +#define CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP + +#include +#include +#include +#include +#include "SDL_events.h" + +#include "InputMapping.hpp" +#include "PadState.hpp" + +namespace Pad { + +inline constexpr double MouseFullTiltCountsPerSecond = 3000.0; +inline constexpr double MouseMinimumStepSeconds = 0.0005; + +class MouseStick { +public: + explicit MouseStick(const InputSettings& settings); + void Move(int deltaX, int deltaY); + void Advance(double seconds); + void Reset(); + double Speed() const; + std::array Deflection() const; + +private: + InputSettings settings; + double pendingX = 0.0; + double pendingY = 0.0; + double pendingSeconds = 0.0; + double speedX = 0.0; + double speedY = 0.0; + double idleSeconds = 0.0; +}; + +struct InputRequests { + bool toggleFullscreen = false; + bool toggleMouse = false; + bool releaseMouse = false; +}; + +class KeyboardMouse { +public: + using Clock = std::chrono::steady_clock; + + explicit KeyboardMouse(InputConfiguration configuration); + InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now); + bool Advance(Clock::time_point now); + void SetMouseMode(bool enabled); + void Release(); + bool MouseMode() const; + PadInputState Compose(const PadInputState& controller) const; + const InputConfiguration& Configuration() const; + +private: + InputConfiguration configuration; + std::vector pressed; + std::vector wheelReleaseTimes; + MouseStick mouse; + Clock::time_point lastAdvance{}; + bool advanced = false; + bool mouseMode = false; +}; + +std::uint8_t StickByte(double deflection); + +} + +#endif diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 6b9a38c833..13f9582b20 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardMouse.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp ) @@ -112,6 +113,17 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(pad_keyboard_mouse_tests tests/KeyboardMouse.cpp src/KeyboardMouse.cpp) + target_include_directories(pad_keyboard_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_keyboard_mouse_tests PRIVATE SDL2-static) + if(MINGW) + target_compile_options(pad_keyboard_mouse_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(pad_keyboard_mouse_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_test(NAME pad_keyboard_mouse COMMAND pad_keyboard_mouse_tests) +endif() + if(BUILD_TESTING) add_test_executable(guest_video_out_wqhd_detection_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutWqhdDetection.cpp) target_include_directories(guest_video_out_wqhd_detection_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index b68b5e5cd3..12b9c4d9d5 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -3,11 +3,9 @@ #include "SDL_events.h" #include "SDL_gamecontroller.h" -#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" -#include #include -#include class DisplayWindow; @@ -30,13 +28,7 @@ private: void applyOutput(); void enableSensors(); - std::vector bindings; - Pad::InputSettings settings; - std::vector pressed; - std::vector wheelReleaseTimes; - std::array mouseStick{128, 128}; - std::chrono::steady_clock::time_point nextMousePoll{}; - bool mouseEnabled = false; + Pad::KeyboardMouse keyboardMouse; SDL_GameController* controller = nullptr; PadInputState controllerState{}; std::uint32_t outputSequence = 0; diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index b407bfeb2e..d974db9e75 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -126,7 +126,13 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc enum class InputSetting { LeftStickDeadzone, RightStickDeadzone, - MouseSensitivity + MouseSensitivity, + MouseVerticalScale, + MouseCurve, + MouseDeadzoneOffset, + MouseMaxSpeed, + MouseSmoothingMs, + MouseDecayMs }; struct InputSettingName { @@ -137,7 +143,13 @@ struct InputSettingName { constexpr auto settingNames = std::array{ InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone}, InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone}, - InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity} + InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity}, + InputSettingName{"MouseVerticalScale", InputSetting::MouseVerticalScale}, + InputSettingName{"MouseCurve", InputSetting::MouseCurve}, + InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset}, + InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed}, + InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs}, + InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs} }; const InputSettingName* findSetting(std::string_view name) { @@ -168,11 +180,33 @@ double parseMouseSensitivity(std::string_view value) { return parsed; } +std::string formatNumber(double value) { + std::array buffer{}; + const auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + return std::string(buffer.data(), result.ptr); +} + +double parseNumber(std::string_view value, std::string_view name, double minimum, double maximum) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != end || !std::isfinite(parsed) || parsed < minimum || parsed > maximum) { + throw std::runtime_error(std::string(name) + " must be a number from " + formatNumber(minimum) + " to " + formatNumber(maximum)); + } + return parsed; +} + void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) { switch (setting) { case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break; case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break; case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break; + case InputSetting::MouseVerticalScale: settings.mouseVerticalScale = parseNumber(value, "mouse vertical scale", 0.1, 10.0); break; + case InputSetting::MouseCurve: settings.mouseCurve = parseNumber(value, "mouse curve", 0.2, 5.0); break; + case InputSetting::MouseDeadzoneOffset: settings.mouseDeadzoneOffset = parseNumber(value, "mouse deadzone offset", 0.0, 0.9); break; + case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break; + case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break; + case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break; } } diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp new file mode 100644 index 0000000000..d8cf0e3d7c --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -0,0 +1,193 @@ +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#include +#include + +Pad::MouseStick::MouseStick(const InputSettings& settings) : settings(settings) {} + +void Pad::MouseStick::Move(int deltaX, int deltaY) { + pendingX += deltaX; + pendingY += deltaY; +} + +void Pad::MouseStick::Advance(double seconds) { + if (!(seconds > 0.0)) return; + pendingSeconds += seconds; + if (pendingSeconds < MouseMinimumStepSeconds) return; + const double step = pendingSeconds; + pendingSeconds = 0.0; + const bool moved = pendingX != 0.0 || pendingY != 0.0; + const double scale = settings.mouseSensitivity / (MouseFullTiltCountsPerSecond * step); + const double targetX = pendingX * scale; + const double targetY = pendingY * scale * settings.mouseVerticalScale; + pendingX = 0.0; + pendingY = 0.0; + idleSeconds = moved ? 0.0 : idleSeconds + step; + if (idleSeconds * 1000.0 >= settings.mouseDecayMs) { + speedX = 0.0; + speedY = 0.0; + return; + } + const double keep = settings.mouseSmoothingMs > 0.0 ? std::exp(-step * 1000.0 / settings.mouseSmoothingMs) : 0.0; + speedX = targetX + (speedX - targetX) * keep; + speedY = targetY + (speedY - targetY) * keep; + const double speed = std::hypot(speedX, speedY); + if (speed > settings.mouseMaxSpeed) { + speedX *= settings.mouseMaxSpeed / speed; + speedY *= settings.mouseMaxSpeed / speed; + } +} + +void Pad::MouseStick::Reset() { + pendingX = 0.0; + pendingY = 0.0; + pendingSeconds = 0.0; + speedX = 0.0; + speedY = 0.0; + idleSeconds = 0.0; +} + +double Pad::MouseStick::Speed() const { + return std::hypot(speedX, speedY); +} + +std::array Pad::MouseStick::Deflection() const { + const double speed = std::hypot(speedX, speedY); + if (speed <= 0.0) return {0.0, 0.0}; + const double shaped = std::pow(std::min(speed, 1.0), settings.mouseCurve); + const double deflection = settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * shaped; + return {speedX / speed * deflection, speedY / speed * deflection}; +} + +std::uint8_t Pad::StickByte(double deflection) { + const double clamped = std::clamp(deflection, -1.0, 1.0); + return static_cast(std::lround(clamped < 0.0 ? 128.0 + clamped * 128.0 : 128.0 + clamped * 127.0)); +} + +Pad::KeyboardMouse::KeyboardMouse(InputConfiguration configuration) + : configuration(std::move(configuration)), + pressed(this->configuration.bindings.size()), + wheelReleaseTimes(this->configuration.bindings.size()), + mouse(this->configuration.settings) {} + +const Pad::InputConfiguration& Pad::KeyboardMouse::Configuration() const { + return configuration; +} + +bool Pad::KeyboardMouse::MouseMode() const { + return mouseMode; +} + +bool Pad::KeyboardMouse::Advance(Clock::time_point now) { + if (!advanced) { + advanced = true; + lastAdvance = now; + } + if (now > lastAdvance) { + const double seconds = std::chrono::duration(now - lastAdvance).count(); + lastAdvance = now; + if (mouseMode) mouse.Advance(seconds); + } + bool released = false; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (configuration.bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; + pressed[index] = false; + wheelReleaseTimes[index] = {}; + released = true; + } + return released; +} + +void Pad::KeyboardMouse::SetMouseMode(bool enabled) { + mouseMode = enabled; + mouse.Reset(); +} + +void Pad::KeyboardMouse::Release() { + std::fill(pressed.begin(), pressed.end(), false); + std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), Clock::time_point{}); +} + +Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock::time_point now) { + InputRequests requests; + if (event.type == SDL_WINDOWEVENT) { + if (event.window.event != SDL_WINDOWEVENT_FOCUS_LOST && event.window.event != SDL_WINDOWEVENT_CLOSE) return requests; + Release(); + requests.releaseMouse = mouseMode; + return requests; + } + if (event.type == SDL_MOUSEMOTION) { + if (mouseMode) mouse.Move(event.motion.xrel, event.motion.yrel); + return requests; + } + if (event.type == SDL_MOUSEWHEEL) { + int direction = (event.wheel.y > 0) - (event.wheel.y < 0); + if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; + if (direction == 0) return requests; + const auto releaseTime = now + std::chrono::milliseconds(WheelPressDurationMs); + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + const auto& binding = configuration.bindings[index]; + if (binding.wheelDirection == 0) continue; + pressed[index] = binding.wheelDirection == direction; + wheelReleaseTimes[index] = pressed[index] ? releaseTime : Clock::time_point{}; + } + return requests; + } + const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; + const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!keyboard && !mouseButton) return requests; + if (keyboard && event.key.repeat != 0) return requests; + const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + const auto& binding = configuration.bindings[index]; + const bool matches = keyboard + ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode + : 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::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; + pressed[index] = down; + } + return requests; +} + +PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const { + PadInputState state = controller; + std::array negative{}; + std::array positive{}; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (!pressed[index]) continue; + const auto& binding = configuration.bindings[index]; + switch (binding.control) { + case InputControl::Button: + state.buttons |= static_cast(binding.button); + if (binding.button == PadButton::L2) state.analogButtonsL2 = 255; + if (binding.button == PadButton::R2) state.analogButtonsR2 = 255; + break; + case InputControl::LeftStickLeft: negative[0] = true; break; + case InputControl::LeftStickRight: positive[0] = true; break; + case InputControl::LeftStickUp: negative[1] = true; break; + case InputControl::LeftStickDown: positive[1] = true; break; + case InputControl::RightStickLeft: negative[2] = true; break; + case InputControl::RightStickRight: positive[2] = true; break; + case InputControl::RightStickUp: negative[3] = true; break; + case InputControl::RightStickDown: positive[3] = true; break; + case InputControl::TouchLeft: state.touchLeft = true; break; + case InputControl::TouchRight: state.touchRight = true; break; + case InputControl::ToggleMouse: break; + case InputControl::ToggleFullscreen: break; + } + } + for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { + if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; + } + if (mouseMode) { + const auto deflection = mouse.Deflection(); + state.sticks[2] = StickByte(deflection[0]); + state.sticks[3] = StickByte(deflection[1]); + } + if (state.analogButtonsL2 != 0) state.buttons |= static_cast(PadButton::L2); + if (state.analogButtonsR2 != 0) state.buttons |= static_cast(PadButton::R2); + return state; +} diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5e5f7690a7..aff446317a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -17,8 +17,7 @@ PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { } -PadInput::PadInput(Pad::InputConfiguration configuration) - : bindings(std::move(configuration.bindings)), settings(configuration.settings), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { +PadInput::PadInput(Pad::InputConfiguration configuration) : keyboardMouse(std::move(configuration)) { openFirstAvailableController(); } @@ -112,13 +111,10 @@ void PadInput::applyOutput() { } void PadInput::setMouseMode(bool enabled) { + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseEnabled = enabled; - mouseStick = {128, 128}; - nextMousePoll = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::MousePollIntervalMs); + keyboardMouse.SetMouseMode(enabled); } PadInputState PadInput::sampleController() const { @@ -160,8 +156,8 @@ PadInputState PadInput::sampleController() const { const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; - const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, settings.leftStickDeadzonePercent); - const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, settings.rightStickDeadzonePercent); + const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, keyboardMouse.Configuration().settings.leftStickDeadzonePercent); + const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, keyboardMouse.Configuration().settings.rightStickDeadzonePercent); result.sticks = {left[0], left[1], right[0], right[1]}; switch (SDL_GameControllerGetType(controller)) { case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break; @@ -213,137 +209,37 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { publish(); return; } - if (event.type == SDL_WINDOWEVENT && (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST || event.window.event == SDL_WINDOWEVENT_CLOSE)) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); - if (mouseEnabled) setMouseMode(false); + if (event.type == SDL_WINDOWEVENT) { + if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); publish(); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach // its window (a stray press advances the title into another stage). static const bool ignoreInput = std::getenv("APS5_NO_PAD_INPUT") != nullptr; - if (ignoreInput && event.type == SDL_MOUSEWHEEL) return; - if (event.type == SDL_MOUSEWHEEL) { - int direction = (event.wheel.y > 0) - (event.wheel.y < 0); - if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; - if (direction == 0) return; - const auto releaseTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::WheelPressDurationMs); - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - if (binding.wheelDirection == 0) continue; - pressed[index] = binding.wheelDirection == direction; - wheelReleaseTimes[index] = pressed[index] ? releaseTime : std::chrono::steady_clock::time_point{}; - } - publish(); - return; - } - const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; - const bool mouse = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; - if (!keyboard && !mouse) return; - if (ignoreInput) return; - if (keyboard && event.key.repeat != 0) return; - const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - const bool matches = keyboard - ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode - : binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast(event.button.button); - - if (!matches) continue; - if (binding.control == Pad::InputControl::ToggleFullscreen) { - if (keyboard && down && !pressed[index] && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); - } - if (binding.control == Pad::InputControl::ToggleMouse && down && !pressed[index]) setMouseMode(!mouseEnabled); - pressed[index] = down; - } - publish(); + const bool input = event.type == SDL_MOUSEWHEEL || event.type == SDL_MOUSEMOTION || event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || + event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!input || ignoreInput) 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.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); + if (event.type != SDL_MOUSEMOTION) publish(); } void PadInput::Update() { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); - if (controller != nullptr) { - controllerState = sampleController(); - publish(); - } - const auto now = std::chrono::steady_clock::now(); - bool released = false; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; - pressed[index] = false; - wheelReleaseTimes[index] = {}; - released = true; - } - if (released) publish(); - if (!mouseEnabled) return; - if (SDL_GetKeyboardFocus() == nullptr) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); + if (controller != nullptr) controllerState = sampleController(); + if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { + keyboardMouse.Release(); setMouseMode(false); publish(); return; } - if (now < nextMousePoll) return; - nextMousePoll = now + std::chrono::milliseconds(Pad::MousePollIntervalMs); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseStick = {128, 128}; - if (deltaX != 0 || deltaY != 0) { - const double distance = std::hypot(deltaX, deltaY); - const double scale = std::clamp(distance * settings.mouseSensitivity + 16.0, 64.0, 128.0) / distance; - const auto mapAxis = [scale](int delta) { return static_cast(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); }; - mouseStick = {mapAxis(deltaX), mapAxis(deltaY)}; - } - publish(); + const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); + if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(); } void PadInput::publish() { - PadInputState state; - state.buttons = controllerState.buttons; - state.sticks = controllerState.sticks; - state.analogButtonsL2 = controllerState.analogButtonsL2; - state.analogButtonsR2 = controllerState.analogButtonsR2; - state.hasMotion = controllerState.hasMotion; - state.accel = controllerState.accel; - state.gyro = controllerState.gyro; - state.touch = controllerState.touch; - state.deviceKind = controllerState.deviceKind; - - std::array negative{}; - std::array positive{}; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (!pressed[index]) continue; - const auto& binding = bindings[index]; - switch (binding.control) { - case Pad::InputControl::Button: - state.buttons |= static_cast(binding.button); - if (binding.button == Pad::PadButton::L2) state.analogButtonsL2 = 255; - if (binding.button == Pad::PadButton::R2) state.analogButtonsR2 = 255; - break; - case Pad::InputControl::LeftStickLeft: negative[0] = true; break; - case Pad::InputControl::LeftStickRight: positive[0] = true; break; - case Pad::InputControl::LeftStickUp: negative[1] = true; break; - case Pad::InputControl::LeftStickDown: positive[1] = true; break; - case Pad::InputControl::RightStickLeft: negative[2] = true; break; - case Pad::InputControl::RightStickRight: positive[2] = true; break; - case Pad::InputControl::RightStickUp: negative[3] = true; break; - case Pad::InputControl::RightStickDown: positive[3] = true; break; - case Pad::InputControl::TouchLeft: state.touchLeft = true; break; - case Pad::InputControl::TouchRight: state.touchRight = true; break; - case Pad::InputControl::ToggleMouse: break; - case Pad::InputControl::ToggleFullscreen: break; - } - } - for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { - if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; - } - if (mouseEnabled) { - state.sticks[2] = mouseStick[0]; - state.sticks[3] = mouseStick[1]; - } - if (state.analogButtonsL2 != 0) state.buttons |= static_cast(Pad::PadButton::L2); - if (state.analogButtonsR2 != 0) state.buttons |= static_cast(Pad::PadButton::R2); - PadPublishInput_nid_postfix(state); + PadPublishInput_nid_postfix(keyboardMouse.Compose(controllerState)); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 0a67672ea6..daa2dad4d6 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -75,6 +75,36 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void mouseLookDefault() { + useConfig("Cross = KEY:F\n"); + const auto settings = Pad::LoadInputMapping().settings; + require(settings.mouseVerticalScale == 1.0 && settings.mouseCurve == 1.0 && settings.mouseMaxSpeed == 1.0, "mouse look scales default to 1"); + require(settings.mouseDeadzoneOffset == 0.2 && settings.mouseSmoothingMs == 10.0 && settings.mouseDecayMs == 30.0, "mouse look timing and offset defaults"); +} + +void mouseLookParse() { + useConfig("MouseCurve=0.7 ; softer start\nmousedeadzoneoffset = 0.15\nMouseMaxSpeed = 1.5\nMouseSmoothingMs = 0\nMouseDecayMs = 50\nMouseVerticalScale = 0.5\n"); + const auto settings = Pad::LoadInputMapping().settings; + require(settings.mouseCurve == 0.7, "mouse curve is read"); + require(settings.mouseDeadzoneOffset == 0.15, "mouse deadzone offset is read case-insensitively"); + require(settings.mouseMaxSpeed == 1.5, "mouse max speed is read"); + require(settings.mouseSmoothingMs == 0.0, "a 0 smoothing is accepted"); + require(settings.mouseDecayMs == 50.0, "mouse decay is read"); + require(settings.mouseVerticalScale == 0.5, "mouse vertical scale is read"); +} + +void mouseLookReject() { + require(rejects("MouseCurve = 0.1\n"), "a mouse curve below 0.2 is rejected"); + require(rejects("MouseCurve = steep\n"), "a non-numeric mouse curve is rejected"); + require(rejects("MouseDeadzoneOffset = 0.95\n"), "a deadzone offset above 0.9 is rejected"); + require(rejects("MouseDecayMs =\n"), "an empty decay is rejected"); + require(rejects("MouseDecayMs = 0\n"), "a zero decay is rejected"); + require(rejects("MouseSmoothingMs = 501\n"), "a smoothing above 500 is rejected"); + require(rejects("MouseMaxSpeed = nan\n"), "a non-finite max speed is rejected"); + require(rejects("MouseVerticalScale = 11\n"), "a vertical scale above 10 is rejected"); + require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -108,6 +138,9 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + mouseLookDefault(); + mouseLookParse(); + mouseLookReject(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp new file mode 100644 index 0000000000..93715508cb --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -0,0 +1,318 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" +#include "PadSimulation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using PadSimulation::At; +using PadSimulation::KeyEvent; +using PadSimulation::Sample; +using PadSimulation::Simulate; +using PadSimulation::TimedEvent; + +constexpr double Polls[] = {33.0, 8.0, 1.0}; + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +Pad::InputConfiguration defaults() { + return Pad::InputConfiguration{{Pad::InputMapping.begin(), Pad::InputMapping.end()}, {}}; +} + +Pad::InputConfiguration withSettings(const std::function& change) { + auto configuration = defaults(); + change(configuration.settings); + return configuration; +} + +double axis(std::uint8_t value) { + return value < 128 ? (static_cast(value) - 128.0) / 128.0 : (static_cast(value) - 128.0) / 127.0; +} + +double rightMagnitude(const Sample& sample) { + return std::hypot(axis(sample.state.sticks[2]), axis(sample.state.sticks[3])); +} + +double meanRightX(const std::vector& samples, double fromMs, double toMs) { + double sum = 0.0; + int count = 0; + for (const auto& sample : samples) { + if (sample.ms <= fromMs || sample.ms > toMs) continue; + sum += axis(sample.state.sticks[2]); + ++count; + } + check(count > 0, "no samples in window"); + return sum / count; +} + +double heldMeanRightX(const std::vector& samples, double pollMs, double fromMs, double toMs) { + double sum = 0.0; + for (const auto& sample : samples) { + if (sample.ms >= fromMs - 1e-9 && sample.ms < toMs - 1e-9) sum += axis(sample.state.sticks[2]) * pollMs; + } + return sum / (toMs - fromMs); +} + +std::vector mouseOnThenMotion(double durationMs, double countsPerSecondX, double countsPerSecondY = 0.0) { + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + PadSimulation::AppendMotion(events, 0.0, durationMs, countsPerSecondX, countsPerSecondY); + return events; +} + +double expectedDeflection(const Pad::InputSettings& settings, double normalizedSpeed) { + const double clamped = std::min(normalizedSpeed, settings.mouseMaxSpeed); + return settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * std::pow(std::min(clamped, 1.0), settings.mouseCurve); +} + +void testCenteredWithoutMotion() { + for (const double poll : Polls) { + const auto samples = Simulate(defaults(), {PadSimulation::MouseModeAt(0.0, true)}, poll, 300.0); + for (const auto& sample : samples) { + check(sample.state.sticks[2] == 128 && sample.state.sticks[3] == 128, "a resting mouse leaves the stick centered"); + } + } +} + +void testSteadyDeflectionFollowsCurve(const Pad::InputConfiguration& configuration, const char* name) { + const auto& settings = configuration.settings; + for (const double speed : {0.05, 0.25, 0.5, 0.75, 0.95, 3.0}) { + const double countsPerSecond = speed * Pad::MouseFullTiltCountsPerSecond / settings.mouseSensitivity; + const double expected = expectedDeflection(settings, speed); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(600.0, countsPerSecond), poll, 600.0); + const double measured = meanRightX(samples, 300.0, 600.0); + if (std::abs(measured - expected) > 0.02) { + throw std::runtime_error(std::string(name) + ": speed " + std::to_string(speed) + " at " + std::to_string(poll) + " ms polls gives " + + std::to_string(measured) + ", expected " + std::to_string(expected)); + } + } + } +} + +void testDeflectionCurve() { + testSteadyDeflectionFollowsCurve(defaults(), "defaults"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 2.0; s.mouseDeadzoneOffset = 0.1; s.mouseSensitivity = 2.0; }), "quadratic"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 0.5; s.mouseDeadzoneOffset = 0.0; }), "square root"); +} + +void testDeadzoneOffset() { + const auto configuration = defaults(); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(400.0, 40.0), poll, 400.0); + bool moved = false; + for (const auto& sample : samples) { + const double magnitude = rightMagnitude(sample); + if (magnitude == 0.0) continue; + moved = true; + check(magnitude >= configuration.settings.mouseDeadzoneOffset - 0.01, "the slowest movement still clears the deadzone offset at " + std::to_string(poll) + " ms polls"); + } + check(moved, "slow movement moves the stick"); + } + const auto samples = Simulate(withSettings([](auto& s) { s.mouseDeadzoneOffset = 0.0; }), mouseOnThenMotion(400.0, 40.0), 8.0, 400.0); + check(meanRightX(samples, 200.0, 400.0) < 0.05, "without an offset slow movement stays small"); +} + +double centeredAfter(const std::vector& samples, double stopMs) { + double lastMoving = stopMs; + double previous = 2.0; + for (const auto& sample : samples) { + if (sample.ms <= stopMs) continue; + const double magnitude = rightMagnitude(sample); + check(magnitude <= previous + 1e-9, "the stick never grows after the mouse stops"); + previous = magnitude; + if (magnitude != 0.0) lastMoving = sample.ms; + } + return lastMoving - stopMs; +} + +void testDecay() { + const auto configuration = defaults(); + for (const double speed : {0.3, 1.0}) { + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, speed * Pad::MouseFullTiltCountsPerSecond), poll, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail < configuration.settings.mouseDecayMs + poll, "stick centered within decay plus one poll at " + std::to_string(poll) + " ms polls, took " + std::to_string(tail)); + check(samples.back().state.sticks[2] == 128 && samples.back().state.sticks[3] == 128, "stick returns exactly to center"); + } + } + const auto slow = withSettings([](auto& s) { s.mouseDecayMs = 120.0; }); + const auto samples = Simulate(slow, mouseOnThenMotion(300.0, Pad::MouseFullTiltCountsPerSecond), 1.0, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail >= 100.0 && tail < 121.0, "a longer decay keeps the stick moving longer"); +} + +void testMaxSpeedClamp() { + const auto configuration = defaults(); + constexpr double StopMs = 264.0; + for (const double poll : Polls) { + const auto flick = Simulate(configuration, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + const auto full = Simulate(configuration, mouseOnThenMotion(StopMs, 1.5 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + check(flick[flick.size() / 3].state.sticks[2] == 255, "a fast flick reaches full tilt"); + check(centeredAfter(flick, StopMs) <= centeredAfter(full, StopMs) + 1e-9, "a flick faster than full tilt does not extend the tail"); + double flickSum = 0.0; + double fullSum = 0.0; + for (std::size_t index = 0; index < flick.size(); ++index) { + if (flick[index].ms <= StopMs) continue; + flickSum += rightMagnitude(flick[index]); + fullSum += rightMagnitude(full[index]); + } + check(std::abs(flickSum - fullSum) <= 0.02 * std::max(1.0, fullSum), "the stick decays from a flick like from full tilt at " + std::to_string(poll) + " ms polls"); + } + const auto headroom = withSettings([](auto& s) { s.mouseMaxSpeed = 3.0; }); + const auto flick = Simulate(headroom, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + const auto full = Simulate(defaults(), mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + check(rightMagnitude(flick[275]) > rightMagnitude(full[275]), "a higher max speed lets a flick hold full tilt longer"); +} + +void testSmoothing() { + const auto instant = Simulate(withSettings([](auto& s) { s.mouseSmoothingMs = 0.0; }), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 8.0, 100.0); + check(std::abs(axis(instant[1].state.sticks[2]) - expectedDeflection(defaults().settings, 0.5)) < 0.02, "without smoothing the stick follows at once"); + const auto smoothed = Simulate(defaults(), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 1.0, 100.0); + const double early = (axis(smoothed[9].state.sticks[2]) - 0.2) / 0.8; + check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant"); +} + +std::vector waveMotion(double base, double swing) { + constexpr double Pi = 3.14159265358979323846; + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + for (int segment = 0; segment < 200; ++segment) { + const double speed = (base + swing * std::sin(2.0 * Pi * segment * 8.0 / 800.0)) * Pad::MouseFullTiltCountsPerSecond; + PadSimulation::AppendMotion(events, segment * 8.0, 8.0, speed, 0.3 * speed); + } + PadSimulation::AppendMotion(events, 1700.0, 200.0, 250.0, -120.0); + return events; +} + +double integratedRightMagnitude(const std::vector& samples, double pollMs) { + double sum = 0.0; + for (const auto& sample : samples) sum += rightMagnitude(sample) * pollMs / 1000.0; + return sum; +} + +void testPollRateIndependence() { + for (const auto& [name, events] : {std::pair{"one-way wave", waveMotion(0.5, 0.3)}, std::pair{"back-and-forth wave", waveMotion(0.0, 0.6)}}) { + std::vector> runs; + for (const double poll : Polls) runs.push_back(Simulate(defaults(), events, poll, 2112.0)); + const double reference = integratedRightMagnitude(runs[2], Polls[2]); + for (std::size_t run = 0; run < runs.size(); ++run) { + const double integrated = integratedRightMagnitude(runs[run], Polls[run]); + check(std::abs(integrated - reference) <= 0.02 * reference, std::string(name) + ": total deflection at " + std::to_string(Polls[run]) + " ms polls is " + + std::to_string(integrated) + ", 1 ms gives " + std::to_string(reference)); + const double tolerance = 0.03 + 0.0025 * Polls[run]; + for (double window = 0.0; window + 264.0 <= 2112.0; window += 264.0) { + const double mean = heldMeanRightX(runs[run], Polls[run], window, window + 264.0); + const double expected = heldMeanRightX(runs[2], Polls[2], window, window + 264.0); + check(std::abs(mean - expected) < tolerance, std::string(name) + ": held deflection at " + std::to_string(Polls[run]) + " ms polls from " + + std::to_string(window) + " ms is " + std::to_string(mean) + ", 1 ms gives " + std::to_string(expected)); + } + } + } +} + +void testVerticalScale() { + const auto configuration = withSettings([](auto& s) { s.mouseVerticalScale = 0.5; s.mouseDeadzoneOffset = 0.0; }); + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, 0.0, 0.8 * Pad::MouseFullTiltCountsPerSecond), 8.0, 300.0); + const double y = axis(samples.back().state.sticks[3]); + check(std::abs(y - 0.4) < 0.02, "vertical scale halves the vertical deflection"); +} + +void testMouseModeOff() { + std::vector events; + PadSimulation::AppendMotion(events, 0.0, 300.0, Pad::MouseFullTiltCountsPerSecond, 0.0); + for (const auto& sample : Simulate(defaults(), events, 8.0, 300.0)) { + check(sample.state.sticks[2] == 128, "motion without mouse mode leaves the stick alone"); + } + events.insert(events.begin(), At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true))); + events.insert(events.begin() + 1, At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false))); + const auto samples = Simulate(defaults(), events, 8.0, 300.0); + check(samples.back().state.sticks[2] == 255, "middle click turns mouse mode on"); +} + +void testKeyboardSticksUnchanged() { + const std::vector events{ + At(0.0, KeyEvent(SDL_SCANCODE_W, true)), + At(10.0, KeyEvent(SDL_SCANCODE_D, true)), + At(20.0, KeyEvent(SDL_SCANCODE_A, true)), + At(30.0, KeyEvent(SDL_SCANCODE_W, false)), + At(30.0, KeyEvent(SDL_SCANCODE_D, false)), + At(40.0, KeyEvent(SDL_SCANCODE_A, false))}; + const auto samples = Simulate(defaults(), events, 1.0, 50.0); + check(samples[4].state.sticks[1] == 0 && samples[4].state.sticks[0] == 128, "W tilts the left stick fully up"); + check(samples[14].state.sticks[0] == 255, "D tilts the left stick fully right"); + check(samples[24].state.sticks[0] == 128, "A and D together cancel"); + check(samples[34].state.sticks[0] == 0 && samples[34].state.sticks[1] == 128, "A alone tilts fully left"); + check(samples[44].state.sticks[0] == 128, "released keys center the stick"); +} + +void testWheelPress() { + const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1))}, 1.0, 200.0); + const auto up = static_cast(Pad::PadButton::Up); + check((samples[40].state.buttons & up) != 0, "wheel up presses Up"); + check((samples[120].state.buttons & up) == 0, "wheel press releases after its duration"); +} + + +void testWheelFocusLoss() { + const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1)), At(10.0, KeyEvent(SDL_SCANCODE_W, true)), At(20.0, PadSimulation::FocusLostEvent())}, 1.0, 40.0); + check(samples[15].state.sticks[1] == 0, "a held key tilts the stick"); + check(samples[30].state.sticks[1] == 128, "losing focus lets go of held keys"); + check((samples[30].state.buttons & static_cast(Pad::PadButton::Up)) == 0, "losing focus lets go of the wheel press"); +} + +void testFocusLossReleasesMouse() { + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.SetMouseMode(true); + const auto requests = keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()); + check(requests.releaseMouse, "losing focus asks to release the captured mouse"); + keyboardMouse.SetMouseMode(false); + check(!keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()).releaseMouse, "nothing to release without capture"); +} + +void testToggleRequests() { + Pad::KeyboardMouse keyboardMouse(defaults()); + const auto now = Pad::KeyboardMouse::Clock::now(); + 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"); +} + +} + +int main() { + try { + const std::pair> tests[] = { + {"centered without motion", testCenteredWithoutMotion}, + {"deflection curve", testDeflectionCurve}, + {"deadzone offset", testDeadzoneOffset}, + {"decay", testDecay}, + {"max speed clamp", testMaxSpeedClamp}, + {"smoothing", testSmoothing}, + {"poll rate independence", testPollRateIndependence}, + {"vertical scale", testVerticalScale}, + {"mouse mode off", testMouseModeOff}, + {"keyboard sticks unchanged", testKeyboardSticksUnchanged}, + {"wheel press", testWheelPress}, + {"wheel and keys on focus loss", testWheelFocusLoss}, + {"focus loss releases the mouse", testFocusLossReleasesMouse}, + {"toggle requests", testToggleRequests}, + }; + for (const auto& [name, test] : tests) { + test(); + std::printf("pad_keyboard_mouse: %s passed\n", name); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "pad_keyboard_mouse: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp new file mode 100644 index 0000000000..75e0168901 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp @@ -0,0 +1,129 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP + +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#include +#include + +#include "SDL_keyboard.h" + +namespace PadSimulation { + +struct TimedEvent { + double ms = 0.0; + SDL_Event event{}; + bool setMouseMode = false; + bool mouseMode = false; + bool release = false; +}; + +struct Sample { + double ms = 0.0; + PadInputState state; +}; + +inline SDL_Event MotionEvent(int deltaX, int deltaY) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.xrel = deltaX; + event.motion.yrel = deltaY; + return event; +} + +inline SDL_Event KeyEvent(SDL_Scancode key, bool down) { + SDL_Event event{}; + event.type = down ? SDL_KEYDOWN : SDL_KEYUP; + event.key.keysym.scancode = key; + event.key.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event ButtonEvent(Uint8 button, bool down) { + SDL_Event event{}; + event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; + event.button.button = button; + event.button.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event FocusLostEvent() { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + return event; +} + +inline SDL_Event WheelEvent(int direction) { + SDL_Event event{}; + event.type = SDL_MOUSEWHEEL; + event.wheel.y = direction; + event.wheel.direction = SDL_MOUSEWHEEL_NORMAL; + return event; +} + +inline TimedEvent At(double ms, const SDL_Event& event) { + return {ms, event, false, false, false}; +} + +inline TimedEvent MouseModeAt(double ms, bool enabled) { + return {ms, SDL_Event{}, true, enabled, false}; +} + +inline TimedEvent ReleaseAt(double ms) { + return {ms, SDL_Event{}, false, false, true}; +} + +inline void AppendMotion(std::vector& events, double startMs, double durationMs, double countsPerSecondX, double countsPerSecondY, double reportHz = 1000.0) { + const double periodMs = 1000.0 / reportHz; + double owedX = 0.0; + double owedY = 0.0; + for (double ms = startMs + periodMs; ms <= startMs + durationMs + 1e-9; ms += periodMs) { + owedX += countsPerSecondX * periodMs / 1000.0; + owedY += countsPerSecondY * periodMs / 1000.0; + const int deltaX = static_cast(std::trunc(owedX)); + const int deltaY = static_cast(std::trunc(owedY)); + owedX -= deltaX; + owedY -= deltaY; + if (deltaX != 0 || deltaY != 0) events.push_back(At(ms, MotionEvent(deltaX, deltaY))); + } +} + +inline std::vector Simulate(const Pad::InputConfiguration& configuration, const std::vector& events, double pollMs, double endMs) { + using Clock = Pad::KeyboardMouse::Clock; + const auto origin = Clock::time_point{} + std::chrono::hours(1); + const auto at = [origin](double ms) { + return origin + std::chrono::duration_cast(std::chrono::duration(ms)); + }; + Pad::KeyboardMouse keyboardMouse(configuration); + keyboardMouse.Advance(at(0.0)); + std::vector samples; + std::size_t next = 0; + const auto steps = static_cast(std::floor(endMs / pollMs + 1e-9)); + for (long long step = 1; step <= steps; ++step) { + const double ms = static_cast(step) * pollMs; + const auto now = at(ms); + for (; next < events.size() && events[next].ms <= ms + 1e-9; ++next) { + const auto& timed = events[next]; + if (timed.release) { + keyboardMouse.Release(); + continue; + } + if (timed.setMouseMode) { + keyboardMouse.SetMouseMode(timed.mouseMode); + continue; + } + const auto requests = keyboardMouse.HandleEvent(timed.event, now); + if (requests.toggleMouse) keyboardMouse.SetMouseMode(!keyboardMouse.MouseMode()); + if (requests.releaseMouse) keyboardMouse.SetMouseMode(false); + } + keyboardMouse.Advance(now); + samples.push_back({ms, keyboardMouse.Compose(PadInputState{})}); + } + return samples; +} + +} + +#endif diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 950c7b0b4e..d61e36d70a 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -37,7 +37,13 @@ The same file also accepts these settings, each at most once, in the form `Setti | Setting | Value | Default | |---------|-------|---------| | `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` | -| `MouseSensitivity` | Number greater than 0 and at most 10 that scales mouse movement on the right stick while the mouse is captured (`ToggleMouse`). | `1` | +| `MouseSensitivity` | Number greater than 0 and at most 10. At 1, moving the mouse 3000 counts per second tilts the stick fully, about 5 cm per second at 1600 DPI; a larger number needs less movement. | `1` | +| `MouseVerticalScale` | Number from 0.1 to 10 that multiplies vertical mouse speed, for games that pitch slower or faster than they turn. | `1` | +| `MouseCurve` | Number from 0.2 to 5, the response exponent. Above 1 gives finer control at low speed; below 1 reaches large tilts sooner. | `1` | +| `MouseDeadzoneOffset` | Number from 0 to 0.9, the smallest tilt any movement produces as a fraction of full tilt. Set it just above the game's own deadzone: raise it if slow movement doesn't move the camera, lower it if small movements jump. | `0.2` | +| `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | +| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 0 follows every mouse report directly. | `10` | +| `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` | ```ini LeftStickDeadzone = 12 @@ -45,4 +51,20 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Mouse as the right stick + +`ToggleMouse` (the middle mouse button by default) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus. + +The stick tilts in the direction the mouse moves, and further the faster it moves. Speed is measured in counts per second over the time between updates, so the same hand speed gives the same tilt at any frame rate. Any movement tilts the stick at least `MouseDeadzoneOffset`, so slow movement isn't lost inside the game's own deadzone. When the mouse stops, the stick eases back and is centered `MouseDecayMs` after the last movement. + +For example, for a game with a small deadzone that needs fast turns: + +```ini +MouseSensitivity = 1.6 +MouseDeadzoneOffset = 0.12 +MouseCurve = 1.3 +``` + +Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior. + An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. From bc568a1f6fc63c9044484bc307057745cdf72f93 Mon Sep 17 00:00:00 2001 From: adriadam10 Date: Tue, 13 Oct 2026 17:00:28 +0200 Subject: [PATCH 111/353] fix(input): MouseModeEnabled reads the keyboard/mouse state Co-Authored-By: Claude --- core/libs/prx/libSceVideoOut/include/PadInput.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index 12b9c4d9d5..de1c799e0e 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -15,7 +15,7 @@ public: ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); - bool MouseModeEnabled() const { return mouseEnabled; } + bool MouseModeEnabled() const { return keyboardMouse.MouseMode(); } private: explicit PadInput(Pad::InputConfiguration configuration); From 7370aaf6d02a3b4b3fcd3a68cbe3cbc8334fef11 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Mon, 12 Oct 2026 21:23:18 +0300 Subject: [PATCH 112/353] feat(agc): decode GS_FAST_LAUNCH geometry with one point per subgroup VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1 (mesh-style NGG, as RADV programs it on gfx10.3) was refused. With a point list and GE_CNTL groups of one point, it launches one subgroup per input point and the first thread reads that point's index in the ES vertex id VGPR, which the mesh prologue already provides. The subgroup's primitive limit comes from GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR, and the GS and ES VGPR counts are not required to be full. Fast launch 2, other group sizes or inputs, and indexed fast-launch draws still throw. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 13 +++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 28 ++++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 33 +++++++++++++++++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 68f487b489..7aa1f37fd9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1037,6 +1037,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto& mesh = *state.stages.mesh; const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); + Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported"); if (args == nullptr) { const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; const auto primitives = (draw.indexCount - inputSize) / step + 1u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8a07f6bc87..4426f8c1c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -634,10 +634,12 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { 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"); result.tessellation = tess; } else { - validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state"); + const bool fastLaunch = ((value >> 19u) & 3u) == 1u; + validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state"); const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig); const auto vertices = (group >> 9u) & 0x1ffu; const auto primitives = group & 0x1ffu; + validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); const auto maxVertices = read(queue.context, 0x1ff); const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly"); @@ -648,11 +650,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit}); validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); - validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); + validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; + const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u; + validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); + const auto maxPrimitives = primitives * amplification; + const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; } return result; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 29a7dc8b2a..1682c8bc10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -573,6 +573,34 @@ void ShaderStageTests() { fan.userConfig[0x242] = 3; expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly"); } + { + auto fast = makeState(); + fast.userConfig[0x242] = 1; + fast.context[0x2d5] = 0x482030; + fast.userConfig[0x25b] = 0x201; + fast.context[0x1ff] = 32; + fast.context[0x2ce] = 32; + fast.context[0x2d3] = 64; + fast.context[0x29b] = 2; + fast.context[0x2ab] = 4; + fast.shader[0x8a] = 0x022c0245; + fast.shader[0x8b] = 0x9002a; + const auto state = AgcDriver::Graphics::DecodeState(fast); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry"); + const auto& mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR"); + fast.context[0x2d5] = 0x502030; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state"); + fast.context[0x2d5] = 0x482030; + fast.userConfig[0x25b] = 0x402; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x25b] = 0x201; + fast.userConfig[0x242] = 4; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x242] = 1; + fast.context[0x2d3] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 29891d5980..47db4246d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,13 @@ alignas(256) constexpr auto Wave32GeometryCode = [] { return code; }(); +alignas(256) constexpr std::array FastLaunchCode{ + 0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010, + 0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302, + 0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a, + 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -132,6 +140,8 @@ struct MeshDraw { bool restart = false; std::uint32_t waveSize = 64u; VkCullModeFlags cull = VK_CULL_MODE_NONE; + std::span code = {}; + std::uint32_t stagesRegister = 0x20u; }; bool ReadsNumWorkgroups(const std::vector& words) { @@ -160,7 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw); std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); - const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode; + const std::span code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span(Wave32GeometryCode) : std::span(GeometryCode); const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, @@ -194,7 +204,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -215,6 +225,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; +constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u}; std::size_t FanPixel(float angle, float radius) { const auto x = static_cast((radius * std::cos(angle) + 1.0f) * 0.5f * Width); @@ -344,6 +355,24 @@ int main() { CheckTriangles("non-indexed wave32 triangle list"); } + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + CheckTriangles("fast-launch subgroups"); + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u; + Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle))); + } + bool fastLaunchIndexedRefused = false; + try { + DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + } catch (const std::runtime_error& error) { + fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos; + if (!fastLaunchIndexedRefused) throw; + } + Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + constexpr std::size_t recordBlockBytes = 65536; auto* record = static_cast(::operator new(recordBlockBytes, std::align_val_t{65536})); { From 1a2a4265c13f970000417737188b20b67acaf7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 13 Oct 2026 17:00:51 +0200 Subject: [PATCH 113/353] docs(debt): record the GS_FAST_LAUNCH decode assumptions --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7c5f2193e9..6f1c1f5758 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -5,6 +5,7 @@ - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured +- [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 - [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 - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw From 8f394389aadb15c8233dec0c837443dd7ba2c5fe Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Mon, 12 Oct 2026 15:43:15 -0400 Subject: [PATCH 114/353] fix(libkernel): return errors from pread instead of aborting The POSIX pread export threw on a null buffer, a negative offset and any host read failure, so a refused read terminated the process. Return -1 and set the guest errno the same way pwrite and sceKernelPread already do. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 21 +++++++-------------- core/libs/tests/GuestFileVector.cpp | 11 +++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d9ceb8756b..0db81dd5c3 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -718,22 +718,15 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr) { - APS5_INVALID_ARG_EX; - } - if (offset < 0) { - APS5_INVALID_ARG_EX; - } + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); - if (!destination.Open()) errno = EFAULT; - else if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); - auto n = destination.Open() ? NativePread(d, buf, nbytes, offset) : -1; - if (n < 0 && errno == EBADF) return PosixFailure(GUEST_EBADF); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pread failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + if (!destination.Open()) return PosixFailure(GUEST_EFAULT); + if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + char empty = 0; + const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 370be6be8f..7374e553f8 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -45,6 +45,8 @@ std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelFtruncate(int, std::int64_t); } @@ -176,6 +178,14 @@ int main() { Require(sceKernelPwrite(file, nullptr, 1, 0) == ErrorEfault); Require(sceKernelPread(file, &byte, 1, -1) == ErrorEinval); Require(sceKernelPwrite(file, &byte, 1, -1) == ErrorEinval); + Require(pread_nid_postfix(file, &byte, 1, 3) == 1 && byte == 'B'); + Require(pread_nid_postfix(file, nullptr, 0, 0) == 0); + Require(pread_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pwrite_nid_disambig1_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pread_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pwrite_nid_disambig1_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(pwrite_nid_disambig1_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFtruncate(file, -1) == ErrorEinval); const int readOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); @@ -185,6 +195,7 @@ int main() { const int writeOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_WRONLY, 0); Require(writeOnly >= 0); Require(sceKernelPread(writeOnly, &byte, 1, 0) == ErrorEbadf); + Require(pread_nid_postfix(writeOnly, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(sceKernelClose(writeOnly) == 0); Require(Contents(path) == "01ABCDE7ABCDE"); Require(sceKernelFtruncate(file, 5) == 0); From d7818c9e5568dc8026c5acece5c5585aead24711 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Tue, 13 Oct 2026 10:45:48 -0400 Subject: [PATCH 115/353] fix(agc): accept integer tessellation partitioning A draw with VGT_TF_PARAM.PARTITIONING 0 (PART_INTEGER) was refused by the state decoder and its hull and domain programs by the recompiler. It is now drawn with Vulkan's equal spacing, the mode radeonsi programs as PART_INTEGER, with tessellation factors bounded at 64 instead of fractional-odd's 63. Power-of-two and fractional-even partitioning still throw. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/ShaderValidation.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++- .../tests/execution/Tessellation.cpp | 35 +++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 6 ++-- .../src/ShaderInputInfoBuilder.cpp | 2 +- docs/dev/debt/shader.md | 3 +- 7 files changed, 46 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 25148b44ec..3ded9b5fcd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -445,7 +445,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool quads = state.rectList || state.stages.tessellation->domain == 2u; const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u; mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); + mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8a07f6bc87..81243cd3c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -628,7 +628,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 == 2 && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with fractional-odd partitioning and triangle output are supported by the reference path"); + validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path"); 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 29a7dc8b2a..75b773bd4e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -600,10 +600,12 @@ void ShaderStageTests() { Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently"); - for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { + for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } + queue.context[0x2db] = 0x40041; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently"); queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 925da125e9..895a10e7f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -3,8 +3,10 @@ #include "Recompiler.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" +#include #include #include +#include #include #include #include @@ -384,9 +386,29 @@ void RequireCounterClockwise(AgcDriver::VulkanDevice& device, const ShaderRecomp Require(CoveredPixels(0, false) > 100 && CoveredPixels(2, false) > 100, "counter-clockwise output did not reverse the control points' winding"); } +bool HasInstruction(std::span words, spv::Op opcode, std::uint32_t length, std::uint32_t operand, std::uint32_t value) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && cursor + count <= words.size(), "malformed SPIR-V while scanning a tessellation module"); + if ((words[cursor] & 0xffffu) == opcode && count == length && words[cursor + operand] == value) return true; + cursor += count; + } + return false; +} + +bool HasSpacing(const ShaderRecompiler::RecompileResult& domain, spv::ExecutionMode spacing) { + return HasInstruction(domain.spirv.Words(), spv::OpExecutionMode, 3u, 2u, static_cast(spacing)); +} + +bool HasLevelBound(const ShaderRecompiler::RecompileResult& hull, float bound) { + return HasInstruction(hull.spirv.Words(), spv::OpConstant, 4u, 3u, std::bit_cast(bound)); +} + void RequireLevelClamps(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(-0.5f + 1.0f / 3.0f); const auto stages = CompileTriangles(target, Triangles); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual), "fractional-odd partitioning did not compile to fractional-odd spacing"); + Require(HasLevelBound(stages.hull, 63.0f) && !HasLevelBound(stages.hull, 64.0f), "fractional-odd partitioning did not bound its tessellation factors at 63"); const PatchDraw flattened{Patches, Indices, 1.0f, 1.0f}; Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, flattened); Require(CoveredPixels(2, false) > 100 && CoveredPixels(2, true) == 0, "VGT_HOS_MAX_TESS_LEVEL 1 did not flatten the level-5 patch, centre " + PixelText(PixelAt(Column(2, 0.25f), center))); @@ -418,6 +440,18 @@ void RequirePatchConstants(AgcDriver::VulkanDevice& device, const ShaderRecompil Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); } +void RequireIntegerPartitioning(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + const PatchDraw patches{{3u, 3u, 1u, 0u, 2u}, Indices}; + const auto stages = CompileTriangles(target, patches); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd), "integer partitioning did not compile to equal spacing"); + Require(HasLevelBound(stages.hull, 64.0f) && !HasLevelBound(stages.hull, 63.0f), "integer partitioning did not bound its tessellation factors at 64"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, "integer partitioning: the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "integer partitioning: the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "integer partitioning: the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -521,6 +555,7 @@ int main() { RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); + RequireIntegerPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); device.reset(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 35bdbd6aee..2b9c7e7fe7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -624,7 +624,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if ((tess.domain != 1u && tess.domain != 2u) || tess.partitioning != 2u || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { + if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { throw std::runtime_error("unsupported tessellation domain, partitioning, or output topology"); } state.module.EmitCapability(spv::CapabilityTessellation); @@ -632,7 +632,7 @@ void DefineTessellationExecutionModes(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSpacingFractionalOdd); + state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd); state.module.AddExecutionMode(state.mainFunc, tess.outputTopology == 3u ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw); } } @@ -1282,7 +1282,7 @@ void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& ins const auto raised = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), raised, GlslStd450(state), GLSLstd450FMax, floating, lowest); const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 0u ? 64.0f : 63.0f)); value = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lowest); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 5eab8b6736..4bce08497c 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -180,7 +180,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || tessellation->partitioning != 2u || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index de34303b91..da90eb672e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -83,11 +83,12 @@ - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. - [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. -- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. +- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer or fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. +- [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). From 1c8d89891614841ebae0f05ad538d6a32fb0ba46 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Mon, 12 Oct 2026 15:45:32 -0400 Subject: [PATCH 116/353] fix(shader): place hull input components by their offset in the control-point slot A hull reading input control point i + k at a constant LDS offset took the vec4 component from the whole constant (constant % 16 / 4) instead of from its offset inside that control point's slot. With a local stride that is not a multiple of 16, a read of the next control point (offset 24 + 4 with a 24-byte stride) landed on component 3 instead of 1. Take the component from constant % stride, as the attribute and the general path already do. --- .../tests/execution/Tessellation.cpp | 29 +++++++++++++++++-- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 925da125e9..13a601e004 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -50,6 +50,23 @@ alignas(256) constexpr std::array HullRestoredExecSaved{ 0xe0741000, 0x0202070b, 0xbf810000, }; +alignas(256) constexpr std::array LocalCodeStride24{ + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x16140698, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, +}; + +alignas(256) constexpr std::array HullNextInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x16002898, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0018, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xbe94047e, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, + 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array DomainCode{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, @@ -223,7 +240,14 @@ struct PatchDraw { float maxLevel = 64.0f; }; +alignas(256) constexpr std::array NextInputIndices{ + Corner(0, 0, 0), Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), + Corner(1, 0, 0), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), +}; + const PatchDraw Triangles{Patches, Indices}; +const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; struct Stages { ShaderRecompiler::RecompileResult local; @@ -344,10 +368,10 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { return count; } -void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode) { std::uint32_t cursor = 0; const Stages stages{ - Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), @@ -517,6 +541,7 @@ int main() { RequirePatches(*device, target, HullCode, "hull program"); RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); + RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); RequireCounterClockwise(*device, target); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 35bdbd6aee..da5edd34ca 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -551,7 +551,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); const auto vertex = inPatch(EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride))); - state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % 16u / 4u)); + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); From d569439e1f0b3784ce5d3a4117e6009396f87525 Mon Sep 17 00:00:00 2001 From: screamm Date: Tue, 13 Oct 2026 20:42:14 +0530 Subject: [PATCH 117/353] feat(libSceSsl): add the connection calls Implements connection lifecycle and parameter exports in libSceSsl: sceSslCreateConnection, sceSslConnect, sceSslRead, sceSslWrite, sceSslDeleteConnection, and sceSslSetVerifyCallback. The repository has no TLS client, so a connection records its context, socket, host and verification callback: - sceSslCreateConnection returns a new connection id, or SSL_ERROR_INVALID_ID (0x8095F006) for an unknown context. A null host throws. - sceSslDeleteConnection drops the connection and leaves socket open. - sceSslSetVerifyCallback stores the callback and argument. - sceSslConnect, sceSslRead and sceSslWrite throw where handshake starts, naming the host and the socket peer port. Carried from @screamm with author attribution preserved. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceSsl/CMakeLists.txt | 3 +- core/libs/prx/libSceSsl/Export.cpp | 82 ++++++++++++++++++++++++++ core/libs/tests/GuestSsl.cpp | 75 ++++++++++++++++++++++- docs/dev/debt/libSceSsl.md | 3 +- 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index d66b9716ec..a86833ac89 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSsl PRIVATE libSceNet) configure_windows_unwind(libSceSsl) if(BUILD_TESTING) add_test_executable(guest_ssl_tests ${LIBS_INCLUDE_DIR}/tests/GuestSsl.cpp) target_include_directories(guest_ssl_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libc) + target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libSceNet libc) configure_windows_unwind(guest_ssl_tests) add_test(NAME guest_ssl COMMAND guest_ssl_tests) if(WIN32) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index fb2265f22d..802a543559 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -16,6 +16,8 @@ static std::atomic g_nextHandle{1}; static std::mutex g_poolsMutex; static std::map g_pools; +extern "C" int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen); + namespace { constexpr int ERROR_NOT_FOUND = static_cast(0x8095F004); @@ -47,11 +49,50 @@ struct SslCaList { }; static_assert(sizeof(SslCaList) == 16); +using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); + +struct SslConnection { + int context; + int socket; + std::string host; + SslVerifyCallback verifyCallback; + void* verifyArgument; +}; + +std::map g_connections; + void RequireContext(const char* function, int sslCtxId) { std::lock_guard lock(g_poolsMutex); if (!g_pools.contains(sslCtxId)) throw std::invalid_argument(std::string(function) + ": unknown context"); } +SslConnection& RequireConnection(const char* function, int sslConnectionId) { + const auto connection = g_connections.find(sslConnectionId); + if (connection == g_connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); + if (!g_pools.contains(connection->second.context)) { + throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); + } + return connection->second; +} + +std::string Endpoint(const SslConnection& connection) { + uint8_t address[28]{}; + uint32_t length = sizeof(address); + if (sceNetGetpeername(connection.socket, address, &length) != 0 || length < 4) { + return connection.host + " (socket " + std::to_string(connection.socket) + " has no peer)"; + } + return connection.host + ":" + std::to_string((address[2] << 8) | address[3]); +} + +[[noreturn]] void ThrowNoTransport(const char* function, int sslConnectionId, const char* operation) { + SslConnection connection; + { + std::lock_guard lock(g_poolsMutex); + connection = RequireConnection(function, sslConnectionId); + } + throw std::runtime_error(std::string(function) + ": no TLS transport for " + operation + " with " + Endpoint(connection)); +} + } extern "C" { @@ -97,6 +138,47 @@ int APS5_VABI sceSslTerm_nid_postfix(int ssl_ctx_id) { return 0; } +int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { + if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(sslCtxId)) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_connections[id] = {sslCtxId, sockId, host, nullptr, nullptr}; + return id; +} + +int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::lock_guard lock(g_poolsMutex); + if (!g_connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + return 0; +} + +int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { + std::lock_guard lock(g_poolsMutex); + SslConnection& connection = RequireConnection(__func__, sslConnectionId); + connection.verifyCallback = cbfunc; + connection.verifyArgument = userArg; + return 0; +} + +int APS5_VABI sceSslConnect(int sslConnectionId) { + ThrowNoTransport(__func__, sslConnectionId, "the handshake"); +} + +int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { + (void)buf; + (void)len; + (void)flags; + ThrowNoTransport(__func__, sslConnectionId, "reading"); +} + +int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { + (void)buf; + (void)len; + (void)flags; + ThrowNoTransport(__func__, sslConnectionId, "writing"); +} + int APS5_VABI sceSslClose() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 4d89668ab6..d22355748e 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -1,8 +1,10 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include +#include extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::size_t); @@ -12,7 +14,18 @@ int APS5_VABI sceSslFreeCaCerts(int, void*); int APS5_VABI sceSslGetCaList(int, void*); int APS5_VABI sceSslFreeCaList(int, void*); int APS5_VABI sceSslUnloadCert(int); -int APS5_VABI sceSslSetVerifyCallback(void); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +int APS5_VABI sceSslConnect(int); +int APS5_VABI sceSslRead(int, void*, std::size_t, int); +int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); } struct SslMemoryPoolStats { @@ -36,6 +49,18 @@ static bool Throws(TCall call) { return false; } +template +static bool ThrowsWith(TCall call, const std::string& text) { + try { + call(); + } catch (const std::exception& error) { + return std::string(error.what()).find(text) != std::string::npos; + } + return false; +} + +static int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } + struct SslCaCerts { void* certs; std::size_t num; @@ -90,7 +115,55 @@ int main() { Require(sceSslGetCaList(context, &list) == notFound); Require(list.certs == nullptr && list.num == 0); + constexpr int invalidId = static_cast(0x8095F006); + Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); + + const int listener = sceNetSocket(nullptr, 2, 1, 6); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(sceNetListen(listener, 1) == 0); + std::uint32_t addressSize = address.size(); + Require(sceNetGetsockname(listener, address.data(), &addressSize) == 0); + const std::string endpoint = "example.com:" + std::to_string((address[2] << 8) | address[3]); + const int client = sceNetSocket(nullptr, 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int idle = sceNetSocket(nullptr, 2, 1, 6); + Require(idle >= 0); + + const int connection = sceSslCreateConnection(context, client, "example.com"); + Require(connection > 0 && connection != context); + Require(sceSslSetVerifyCallback(connection, VerifyCallback, &marker) == 0); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0); + char buffer[4]{}; + Require(ThrowsWith([&] { sceSslConnect(connection); }, endpoint)); + Require(ThrowsWith([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); }, endpoint)); + Require(ThrowsWith([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); }, endpoint)); + + const int unconnected = sceSslCreateConnection(context, idle, "example.com"); + Require(unconnected > 0 && unconnected != connection); + Require(ThrowsWith([&] { sceSslConnect(unconnected); }, "example.com (socket")); + Require(sceSslDeleteConnection(unconnected) == 0); + + const int unknown = connection + 1000; + Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); + Require(ThrowsWith([&] { sceSslConnect(unknown); }, "unknown connection")); + Require(ThrowsWith([&] { sceSslRead(unknown, buffer, sizeof(buffer), 0); }, "unknown connection")); + Require(ThrowsWith([&] { sceSslWrite(unknown, buffer, sizeof(buffer), 0); }, "unknown connection")); + Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(Throws([&] { sceSslConnect(unconnected); })); + Require(sceSslTerm_nid_postfix(context) == 0); + Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); + Require(ThrowsWith([&] { sceSslConnect(connection); }, "terminated")); + Require(sceSslCreateConnection(context, client, "example.com") == invalidId); + Require(sceSslDeleteConnection(connection) == 0); + Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceNetSocketClose(idle) == 0); + Require(sceNetSocketClose(client) == 0); + Require(sceNetSocketClose(listener) == 0); + Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); list = {&handle, 3}; diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 0a72ff2bfa..70045e2483 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw with the host and the socket's peer port where the handshake would start. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetCaList, sceSslFreeCaList, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and the 16-byte `SceSslCaList` layout (an array of certificate handles and a 32-bit count) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs) and shadPS4's `OrbisSslCaList`, not confirmed for the PS5. On the console the list holds the certificate authorities the SSL service has; [shadPS4](https://github.com/shadps4-emu/shadPS4/pull/4104) returns a placeholder certificate from `sceSslGetCaCerts` because Rise of the Tomb Raider fails its initialization checks without one. No root certificate bundle is shipped and no loaded certificate is kept, so here a context holds none: `sceSslGetCaList` clears the list and returns `NOT_FOUND`, as `sceSslGetCaCerts` does, `sceSslFreeCaList` accepts only that cleared list, and `sceSslUnloadCert` (imported by PPSA15508) has nothing to drop and is assumed to return 0. A null list returns `0x8095177A`, which `sceSslGetCaCerts` and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/libs/network.cpp) return for a null argument (shadPS4, PS4, returns `0x8095F007`). An unknown context throws (KytyPS5 returns `SSL_ERROR_INVALID_ID`, `0x8095F006`, from `sceSslGetCaCerts`), and so does a list `sceSslGetCaList` did not return @@ -9,4 +10,4 @@ ### Functional -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslEnableVerifyOption](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) From 793bd22f49bf9ec4e8428a38f470f8864b07d29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 13 Oct 2026 17:15:33 +0200 Subject: [PATCH 118/353] fix(libSceSsl): drop the old parameterless connection stubs Co-Authored-By: Claude --- core/libs/prx/libSceSsl/Export.cpp | 30 ------------------------------ core/libs/tests/GuestSsl.cpp | 2 -- 2 files changed, 32 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 802a543559..006534de76 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -261,39 +261,9 @@ int APS5_VABI sceSslGetSubjectName(void) { return 0; } -int APS5_VABI sceSslCreateConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslDeleteConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSslEnableVerifyOption(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslSetVerifyCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslConnect(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslRead(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslWrite(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index d22355748e..bad0d237db 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -76,8 +76,6 @@ int main() { constexpr int notFound = static_cast(0x8095F004); constexpr int invalidArg = static_cast(0x8095177A); int marker = 0; - Require(Throws([] { sceSslSetVerifyCallback(); })); - const int context = sceSslInit_nid_postfix(0x10000); Require(context > 0); SslMemoryPoolStats stats{1, 1, 1, 1}; From 0f7cd5b7ac26c5c704e3c2715a7bd806b608dc9b Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 12 Oct 2026 23:04:58 +0200 Subject: [PATCH 119/353] fix(conventions): skip library test fixtures in the export checks The duplicate-export and silent-stub rules treated every APS5_VABI definition under core/libs/prx/ as a library export, including mocks and callbacks in a library's tests/ directory. Those files are built only into separate test executables, never into the library, so they are not exports. PR #3533 failed CI because a sceKernelGetProcessTime mock in libSceAudioOut/tests was reported as a duplicate of libkernel's export. An sce* fixture with a stub-shaped body was also reported as a silent stub, and a real export that shares a name with a fixture in another library's tests/ was reported as a duplicate. Skip files under a tests directory on both sides of the check, with the same test code() already uses for title-specific and progress.py uses for its function counts. Co-Authored-By: Claude Opus 5.5 --- tools/check_conventions.py | 4 +- .../tests/test_check_conventions_comments.py | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 34b639f166..44bb8eca4d 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -109,7 +109,7 @@ class Check: if core and TITLE.search(text): self.report("title-specific", path, number, TITLE.search(text).group(0)) match = EXPORT.search(bare) - if match and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): + if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() body = function_body(source, number - 1) if body is None: @@ -134,7 +134,7 @@ class Check: _, path, number, text = line.split(":", 3) name = EXPORT.search(text).group(1) own, own_number = exports[name] - if library(path) != library(own) and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(self.show(path).splitlines(), int(number) - 1) is not None: self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 4d3e83c0bc..a00b924fd3 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -245,6 +245,58 @@ class SilentStubSectionTests(unittest.TestCase): self.assertNotIn("[debt-file]", result.stdout + result.stderr) +class TestFixtureExportTests(unittest.TestCase): + def check(self, path, text): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + def write(relative, content): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + + git("init", "-q") + write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + git("add", ".") + git("commit", "-qm", "feat: add libraries") + write(path, text) + git("add", ".") + git("commit", "-qm", "feat: add source") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def test_mock_in_tests_directory_is_not_an_export(self): + result = self.check("core/libs/prx/libSceAudioOut/tests/Shutdown.cpp", + 'extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime() { return 0; }\n') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_named_like_a_test_fixture_is_not_a_duplicate(self): + result = self.check("core/libs/prx/libSceAjm/src/Port.cpp", "int APS5_VABI Process(int value) { return value + 1; }\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_in_another_library_is_still_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + class PullRequestTemplateTests(unittest.TestCase): def findings(self, line): check = Check.__new__(Check) From 13b4de565b8dd82d210ffde23761087b85964ffc Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 14:35:08 +0300 Subject: [PATCH 120/353] fix(libkernel): return the error number from sigwait FreeBSD's sigwait hands the error number back as its return value and leaves errno alone; the guest export returned -1 and set errno. (cherry picked from commit b627f6c9443dd94a6392959f33e52d837ee4cae4) --- core/libs/prx/libkernel/System/src/Signals.cpp | 5 +++-- core/libs/tests/GuestSigtimedwait.cpp | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 01226d305d..72c8ab1a15 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -559,9 +559,10 @@ int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { } int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { - if (set == nullptr || received == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + if (set == nullptr || received == nullptr || !SetHasSignal(*set)) return 22; int guest = 0; - if (WaitForPendingInSet(*set, true, nullptr, &guest) != 0) { *__error_nid_postfix() = GuestEagain; return -1; } + const int result = WaitForPendingInSet(*set, true, nullptr, &guest); + if (result != 0) return result; *received = guest; return 0; } diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 03a9a237b8..347deaf32e 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -96,9 +96,8 @@ int main() { *__error_nid_postfix() = 0; Require(sigwaitinfo_nid_postfix(&empty, &info) == -1 && *__error_nid_postfix() == 22); *__error_nid_postfix() = 0; - Require(sigwait_nid_postfix(&empty, &taken) == -1 && *__error_nid_postfix() == 22 && taken == -1); - *__error_nid_postfix() = 0; - Require(sigwait_nid_postfix(&interrupt, nullptr) == -1 && *__error_nid_postfix() == 22); + Require(sigwait_nid_postfix(&empty, &taken) == 22 && *__error_nid_postfix() == 0 && taken == -1); + Require(sigwait_nid_postfix(&interrupt, nullptr) == 22 && *__error_nid_postfix() == 0); RequireUntouched(info); *__error_nid_postfix() = 0; Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == -1 && *__error_nid_postfix() == 35); From 85c090d49d07a66544401c4b5046266a93fe4b05 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 15:49:45 +0000 Subject: [PATCH 121/353] test(libkernel): verify sigwait preserves errno --- core/libs/tests/GuestSigtimedwait.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 347deaf32e..52a65f57d5 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -95,9 +95,12 @@ int main() { Require(sigwaitinfo_nid_postfix(nullptr, &info) == -1 && *__error_nid_postfix() == 22); *__error_nid_postfix() = 0; Require(sigwaitinfo_nid_postfix(&empty, &info) == -1 && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(sigwait_nid_postfix(&empty, &taken) == 22 && *__error_nid_postfix() == 0 && taken == -1); - Require(sigwait_nid_postfix(&interrupt, nullptr) == 22 && *__error_nid_postfix() == 0); + *__error_nid_postfix() = 71; + Require(sigwait_nid_postfix(nullptr, &taken) == 22 && *__error_nid_postfix() == 71 && taken == -1); + *__error_nid_postfix() = 72; + Require(sigwait_nid_postfix(&empty, &taken) == 22 && *__error_nid_postfix() == 72 && taken == -1); + *__error_nid_postfix() = 73; + Require(sigwait_nid_postfix(&interrupt, nullptr) == 22 && *__error_nid_postfix() == 73); RequireUntouched(info); *__error_nid_postfix() = 0; Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == -1 && *__error_nid_postfix() == 35); From 6f856a7cda16568b4bd7e48e0e4b9029512de4e7 Mon Sep 17 00:00:00 2001 From: NaitikJoshiPro <190247830+NaitikJoshiPro@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:21:45 +0530 Subject: [PATCH 122/353] feat(relinker): executable TLS relocations (TPOFF64, DTPOFF64, DTPMOD64) A PS5 eboot can keep initial-exec TLS as R_X86_64_TPOFF64 GOT relocations (SuperTuxKart, STKT00001, has 13, all without a symbol), which the relocation allowlist rejected. Relocations against the executable's own TLS block (no symbol, or a defined STT_TLS symbol folded into the addend) now go to the Linux ELF as symbolless dynamic relocations that the dynamic loader resolves against where it placed the block; one against an import stays named and is typed STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against the block AlignUp(p_memsz, p_align) below the thread pointer, the offset WindowsTlsBuilder and MacOsImage use, and reject DTPMOD64 and imported TLS. TPOFF32 stays unsupported. New test executable_tls_relocations covers the Linux, Windows and macOS outputs and the rejected cases. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/general/ElfConstants.hpp | 5 + .../windows/WindowsRelocationBuilder.hpp | 3 +- .../src/macos/MacOsMachOPatcher.cpp | 2 +- .../src/windows/WindowsPePatcher.cpp | 2 +- .../src/windows/WindowsRelocationBuilder.cpp | 23 ++- .../src/analysis/ValidationPolicy.cpp | 3 + .../src/output/SysVDynamicSectionBuilder.cpp | 4 +- .../src/pipeline/RelinkerPipeline.cpp | 57 ++++++- .../tests/test_executable_tls_relocations.py | 143 ++++++++++++++++++ 10 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 core/relinker/relinker/tests/test_executable_tls_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index e670c9980f..0a19391d97 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -428,6 +428,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_segments.py $) add_test(NAME linux_executable_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) + add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index dc0099f1d1..e0b0ee4bab 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -63,6 +63,7 @@ inline constexpr std::uint32_t PT_DYNAMIC = 2; inline constexpr std::uint32_t PT_INTERP = 3; inline constexpr std::uint32_t PT_NOTE = 4; inline constexpr std::uint32_t PT_PHDR = 6; +inline constexpr std::uint32_t PT_TLS = 7; inline constexpr std::uint32_t PT_GNU_EH_FRAME = 0x6474e550; inline constexpr std::uint32_t PT_GNU_STACK = 0x6474e551; inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552; @@ -137,6 +138,9 @@ inline constexpr std::uint32_t R_X86_64_RELATIVE = 8; inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9; inline constexpr std::uint32_t R_X86_64_32 = 10; inline constexpr std::uint32_t R_X86_64_32S = 11; +inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16; +inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; +inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; @@ -144,6 +148,7 @@ inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; +inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp index 0d641e8fed..c9d0c656e3 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp @@ -7,7 +7,8 @@ namespace Elfpatcher::Windows { class WindowsRelocationBuilder { public: - PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const; + PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const; std::vector BuildBaseRelocations(const std::vector& targets) const; }; diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 8e76037730..2a56daf6dc 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -46,7 +46,7 @@ std::vector MacOsMachOPatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector #include #include +#include +#include #include +#include namespace Elfpatcher::Windows { -PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const { +PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const { if (dynamicSection.DynSymData.empty() || dynamicSection.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid ELF dynamic symbol table size"); PeRelocations result; @@ -19,7 +23,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto addend = Io::ReadU64(table, offset + 16); const auto type = static_cast(info); const auto symbol = info >> 32; - if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8)) + if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18)) throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target); const auto rva = image.GetRva(target, 8); const auto next = ranges.lower_bound(target); @@ -33,6 +37,21 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom result.BaseRelocations.push_back(rva); continue; } + if (type == 16 || type == 17 || type == 18) { + if (symbol != 0) + throw Domain::RelinkerException("Imported TLS relocation " + std::to_string(type) + " is not supported for this target", target); + if (type == 16) + throw Domain::RelinkerException("Executable DTPMOD64 relocation is not supported for this target", target); + const auto tls = std::find_if(headers.begin(), headers.end(), [](const auto& header) { return header.Type == 7; }); + if (tls == headers.end() || tls->MemorySize == 0) + throw Domain::RelinkerException("TLS relocation without a non-empty PT_TLS segment", target); + const auto alignment = std::max(tls->Alignment, 1); + if (!std::has_single_bit(alignment) || tls->MemorySize > 0x7fff0000u) + throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); + image.RequireWritable(target, 8); + image.WritePointer(target, type == 17 ? addend : addend - Io::AlignUp64(tls->MemorySize, alignment)); + continue; + } if (symbol == 0 || symbol >= dynamicSection.DynSymData.size() / 24) throw Domain::RelinkerException("Invalid import symbol index", target); const auto symbolOffset = static_cast(symbol) * 24; diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 2f2f4f91bc..6cf1c76066 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -29,6 +29,9 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_GOTPCREL, R_X86_64_32, R_X86_64_32S, + R_X86_64_DTPMOD64, + R_X86_64_DTPOFF64, + R_X86_64_TPOFF64, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX, }; diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 65441ef8cb..32cb6523c4 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -84,7 +84,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); - return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC); + const bool tls = ref.RelocationTypeValue == R_X86_64_DTPMOD64 || ref.RelocationTypeValue == R_X86_64_DTPOFF64 + || ref.RelocationTypeValue == R_X86_64_TPOFF64; + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | (tls ? STT_TLS : STT_FUNC)); }; static constexpr std::size_t kRelaEntSize = 24; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index bd98088047..f6a3206e67 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -36,6 +36,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { case 6: return "R_X86_64_GLOB_DAT"; case 7: return "R_X86_64_JUMP_SLOT"; case 10: return "R_X86_64_32"; + case 16: return "R_X86_64_DTPMOD64"; + case 17: return "R_X86_64_DTPOFF64"; + case 18: return "R_X86_64_TPOFF64"; default: { std::ostringstream oss; oss << "UNKNOWN(" << type << ")"; @@ -230,6 +233,31 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return (raw[symOff + 4] & 0xf) == 3 && value == 0; }; + const auto isTlsRelocation = [](const std::uint32_t type) { + return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; + }; + + const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + + const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + throw RelinkerException("Symbol table entry out of bounds", symOff); + std::uint16_t section = 0; + std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); + if (section == SHN_UNDEF) + return std::nullopt; + if ((raw[symOff + 4] & 15) != STT_TLS) + throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return value; + }; + const auto definedTlsSymbol = [&](const std::uint32_t symIdx, const FileByteOffset pos) { + return definedTlsSymbolValue(symIdx, pos).has_value(); + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -252,6 +280,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; } + if (isTlsRelocation(relType) && (symIdx == 0 || definedTlsSymbol(symIdx, pos))) + continue; + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) throw RelinkerException("Symbol table entry out of bounds", symOff); @@ -415,7 +446,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractRelative = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); std::uint64_t rOffset = 0, rInfo = 0; @@ -423,14 +454,32 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); + const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) { appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); + continue; + } + if (!isTlsRelocation(relType)) + continue; + std::uint64_t offset = 0; + if (symIdx != 0) { + const auto value = definedTlsSymbolValue(symIdx, pos); + if (!value) + continue; + offset = *value; + } + if (!hasTlsSegment) + throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos); + if (relType == R_X86_64_DTPMOD64 && rAddend != 0) + throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos); + const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast(offset + static_cast(rAddend)); + appendRela(dynSection.RelaData, rOffset, static_cast(relType), addend); } }; - extractRelative(dynRelaOffset, dynRelaSize); - extractRelative(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize); + extractLocal(dynJmpRelOffset, dynJmpRelSize); std::vector entries; entries.reserve(nidRefs.size()); diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py new file mode 100644 index 0000000000..0747677195 --- /dev/null +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -0,0 +1,143 @@ +"""Initial-exec TLS relocations in the executable (R_X86_64_TPOFF64, DTPOFF64, DTPMOD64), as a PS5 eboot +has them in its GOT. The Linux ELF keeps them dynamic and symbolless, with the variable's offset in the +TLS block as the addend, for the dynamic loader to resolve against where it puts the block; a named one +against a defined STT_TLS symbol is folded the same way, and one against an import stays named, typed +STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against their block, +AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture +from test_macos_output import MachO +from test_tls_function_coverage import pe_bytes_at + +PT_LOAD = 1 +PT_TLS = 7 +DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 +TLS_MEMSZ, TLS_ALIGN = 0x24, 16 +TLS_BLOCK = 0x30 +STRINGS = b"\0Tls\0Data\0ImportedTls\0" +TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 + +NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 + + +def image(relocations, tls=True): + """The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and an 8-byte + TLS template, a symbol table and the given (offset, type, symbol, addend) relocations.""" + data = fixture() + struct.pack_into("> 32) * 24) + end = elf.index(b"\0", strings + name) + result.append((target, info & 0xffffffff, elf[strings + name:end].decode(), symbol_info & 15, addend)) + return result + + +def macho_bytes_at(data, address, size): + for segment in MachO(data).segments: + if segment["vmaddr"] <= address and address + size <= segment["vmaddr"] + segment["filesize"]: + offset = segment["fileoff"] + address - segment["vmaddr"] + return data[offset:offset + size] + raise AssertionError(f"unmapped address {address:#x}") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-exec-tls-") as directory: + local = [(0x1000, TPOFF64, NULL, 4), (0x1008, DTPOFF64, NULL, 8), (0x1010, TPOFF64, TLS_SYMBOL, 2)] + result, output = relink(relinker, directory, "linux", image(local + [(0x1018, DTPMOD64, NULL, 0), + (0x1020, TPOFF64, IMPORTED, 0)])) + assert result.returncode == 0, (result.stdout, result.stderr) + elf = output.read_bytes() + relocations = dynamic_relocations(elf) + tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) + assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), + (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert any(struct.unpack_from(" Date: Tue, 13 Oct 2026 00:03:15 +0300 Subject: [PATCH 123/353] feat(libc): add missing Unity IL2CPP runtime imports and Dinkumware helpers Add missing libc.prx symbols required by Unity IL2CPP runtime: - Implement _Dtest double classification in Math.cpp - Implement _Inf double constant in Math.cpp - Implement sceLibcBacktraceGetBufferSize and sceLibcBacktraceSelf in Backtrace.cpp - Implement _Cnd_signal in Threads.cpp - Implement _ZNSt8ios_base7_AddstdEPS_ in RuntimeSupport.cpp - Implement __pointer_type_info and __function_type_info vtables in TypeInfo.cpp - Remove duplicate stubs from libSceAmpr/Export.cpp - Add unit tests in GuestMath.cpp and GuestLibcThreads.cpp - Document unverified behaviors in docs/dev/debt/libc.md --- core/libs/prx/libSceAmpr/Export.cpp | 21 ------------------ core/libs/prx/libc/CMakeLists.txt | 3 ++- .../libc/include/specifics/itanium/CxxAbi.hpp | 2 ++ core/libs/prx/libc/src/Backtrace.cpp | 16 ++++++++++++++ core/libs/prx/libc/src/Math.cpp | 22 +++++++++++++++++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 4 ++++ core/libs/prx/libc/src/Threads.cpp | 6 +++++ core/libs/prx/libc/src/exception/TypeInfo.cpp | 17 ++++++++++++++ core/libs/tests/GuestLibcThreads.cpp | 9 ++++++++ core/libs/tests/GuestMath.cpp | 20 +++++++++++++++++ docs/dev/debt/libc.md | 1 + 11 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 core/libs/prx/libc/src/Backtrace.cpp diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index f1918902d1..fba6d9efbe 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -898,15 +898,9 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -int APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - [[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { ThrowCppError_nid_no_patch(code); } - int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { void* result = nullptr; if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; @@ -957,16 +951,11 @@ void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } - int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { - return SignalCondition_nid_no_patch(handle); -} - void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } @@ -975,11 +964,6 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { return _Cnd_init_nid_postfix(handle); } -int APS5_VABI _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -995,11 +979,6 @@ int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_ return 0; } -int APS5_VABI _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 8f2342a25b..50a8682684 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -48,6 +48,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RealPath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Threads.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) @@ -214,7 +215,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_libc_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcThreads.cpp) target_include_directories(guest_libc_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_libc_threads_tests PRIVATE libc) + target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) configure_windows_unwind(guest_libc_threads_tests) add_test(NAME guest_libc_threads COMMAND guest_libc_threads_tests) set_tests_properties(guest_libc_threads PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp index 53d45d895b..f4957fd7b3 100644 --- a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp +++ b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp @@ -39,6 +39,8 @@ public: class __pointer_type_info : public __pbase_type_info {}; +class __function_type_info : public std::type_info {}; + class __pointer_to_member_type_info : public __pbase_type_info { public: const __class_type_info* __context; diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp new file mode 100644 index 0000000000..5286225707 --- /dev/null +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -0,0 +1,16 @@ +#include +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 1a835e2fe8..4b4a78f73d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -465,6 +465,19 @@ short APS5_VABI _FDtest_nid_postfix(const float* value) { if (exponent == 0) return fraction != 0 ? Denormal : Zero; return Finite; } + +short APS5_VABI _Dtest_nid_postfix(const double* value) { + constexpr short Denormal = -2, Finite = -1, Zero = 0, Infinite = 1, NotANumber = 2; + if (value == nullptr) throw std::invalid_argument("_Dtest: null value"); + std::uint64_t bits; + std::memcpy(&bits, value, sizeof(bits)); + const auto exponent = bits & 0x7ff0000000000000u; + const auto fraction = bits & 0x000fffffffffffffu; + if (exponent == 0x7ff0000000000000u) return fraction != 0 ? NotANumber : Infinite; + if (exponent == 0) return fraction != 0 ? Denormal : Zero; + return Finite; +} + int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } @@ -526,4 +539,13 @@ int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } +union SonyDconst { + unsigned short _Word[8]; + float _Float; + double _Double; + long double _Long_double; +}; + +static_assert(std::endian::native == std::endian::little); +SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 56a0f8d499..4b7ae591d7 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -192,4 +192,8 @@ unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, un return SonyUmodti3(dividend, divisor); } +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { + NotImplemented_nid_no_patch(__func__); +} + } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index e74bcb5f95..b1d85f9faf 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -294,4 +294,10 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { } } + +int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { + return SignalCondition_nid_no_patch(handle); } + +} + diff --git a/core/libs/prx/libc/src/exception/TypeInfo.cpp b/core/libs/prx/libc/src/exception/TypeInfo.cpp index c745eda722..49ba0e47ee 100644 --- a/core/libs/prx/libc/src/exception/TypeInfo.cpp +++ b/core/libs/prx/libc/src/exception/TypeInfo.cpp @@ -13,6 +13,8 @@ void APS5_VABI DeleteTypeInfo(std::type_info*) { } bool APS5_VABI IsPointerTypeInfo(const std::type_info*) { return false; } +bool APS5_VABI IsPointerTypeInfoTrue(const std::type_info*) { return true; } +bool APS5_VABI IsFunctionTypeInfoTrue(const std::type_info*) { return true; } bool APS5_VABI CatchTypeInfo(const std::type_info* self, const std::type_info* thrown, void** object, unsigned) { if (!self || !thrown || !object) throw std::invalid_argument("invalid RTTI catch arguments"); @@ -65,4 +67,19 @@ extern "C" { LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__si_class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv121__vmi_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__vmi_class_type_info)}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__pointer_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfoTrue +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__function_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfo, + LibcException::IsFunctionTypeInfoTrue +}; } diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index c0516c6857..0aabfe21ce 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -16,6 +16,7 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle); void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); } static void Require(bool value) { if (!value) std::abort(); } @@ -175,9 +176,17 @@ static void ConditionBroadcast() { _Mtx_destroy_nid_postfix(&mutex); } +static void ConditionSignal() { + void* condition = nullptr; + Require(_Cnd_init_nid_postfix(&condition) == success); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); +} + int main() { MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); + ConditionSignal(); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index cad91f320d..34d56fcca0 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -47,6 +47,7 @@ struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); +short APS5_VABI _Dtest_nid_postfix(const double*); int APS5_VABI __fpclassifyf_nid_postfix(float); int APS5_VABI isnanf_nid_postfix(float); int APS5_VABI __signbitl_nid_postfix(long double); @@ -696,6 +697,24 @@ static void CheckFloatMathExports() { Require(isinf_nid_postfix(nan) == 0 && isinf_nid_postfix(1.0) == 0); } +static void CheckDoubleClassification() { + const struct { std::uint64_t bits; short code; } cases[] = { + {0x0000000000000000ull, 0}, {0x8000000000000000ull, 0}, + {0x0000000000000001ull, -2}, {0x800fffffffffffffull, -2}, + {0x0010000000000000ull, -1}, {0xbff0000000000000ull, -1}, {0x7fefffffffffffffull, -1}, + {0x7ff0000000000000ull, 1}, {0xfff0000000000000ull, 1}, + {0x7ff0000000000001ull, 2}, {0x7ff8000000000000ull, 2}, {0xfff8000000000000ull, 2}, + }; + for (const auto& test : cases) { + double value; + std::memcpy(&value, &test.bits, sizeof(value)); + if (_Dtest_nid_postfix(&value) != test.code) { + std::fprintf(stderr, "Guest _Dtest failed for %016llx\n", static_cast(test.bits)); + std::abort(); + } + } +} + static void CheckDoubleRounding(double value, std::int64_t expected, bool invalid) { for (const bool useLlround : {false, true}) { std::feclearexcept(FE_ALL_EXCEPT); @@ -956,4 +975,5 @@ int main() { CheckAtanhf(); CheckHypotSpecialValue(); CheckFmodErrno(); + CheckDoubleClassification(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad..740c9c59c1 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -66,3 +66,4 @@ - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested +- [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. From 1fbb2673c0220b978f35de8d09c7d901485278a3 Mon Sep 17 00:00:00 2001 From: fogydes Date: Tue, 13 Oct 2026 20:45:36 +0530 Subject: [PATCH 124/353] fix(relinker): validate text section address bounds and avoid signed overflow in call-site targets --- core/relinker/relinker/src/analysis/CallSiteResolver.cpp | 4 +++- core/relinker/relinker/tests/CallSiteIndexTests.cpp | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index fbd29c405f..120e7abac6 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -79,6 +79,8 @@ private: std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { if (targetGotOrPltSize > std::numeric_limits::max() - targetGotOrPltAddress) throw RelinkerException("Relocation target range exceeds the address space"); + if (textSection.size() > std::numeric_limits::max() - textSectionVAddr) + throw RelinkerException("Text section range exceeds the address space"); if (textSection.empty()) return {}; if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr) { auto scanner = Codegen::MakeInstructionScanner(); @@ -99,7 +101,7 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector if (dispOff + 4 > textSection.size()) continue; std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); - VirtualAddress target = static_cast(static_cast(instrEnd) + disp); + VirtualAddress target = instrEnd + static_cast(static_cast(disp)); _cachedTargetToInstr.emplace_back(target, m.Offset); } std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp index fceff1c265..c286f1c224 100644 --- a/core/relinker/relinker/tests/CallSiteIndexTests.cpp +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -32,6 +32,13 @@ int main() { try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } catch (const std::exception&) { rejected = true; } Require(rejected); - std::cout << "10 call-site index cases passed\n"; + rejected = false; + try { resolver->ResolveCallSites(code, std::numeric_limits::max(), 0x1000, 8); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + constexpr std::uint64_t highBase = 0x8000000000000000ULL; + std::vector highCode{0xFF, 0x15, 0xFA, 0xFF, 0xFF, 0xFF}; + Require(resolver->ResolveCallSites(highCode, highBase + 0x100, highBase + 0x100, 8) == std::vector{highBase + 0x100}); + std::cout << "12 call-site index cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } From ac82f3c6a9d8ecf083ed20a8f285afbc38ead4f6 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 18:19:05 +0300 Subject: [PATCH 125/353] fix(libkernel): switch the host stack bounds in siglongjmp on Windows longjmp moves the Windows stack bounds onto the stack of the target jump buffer so that exceptions and stack probes keep working after a jump between guest stacks. siglongjmp restored the same registers without that step, so a jump from a context stack back to the thread stack left the thread bounds describing the stack it had left. --- .../prx/libkernel/System/src/SignalSets.cpp | 10 ++++++++ core/libs/tests/GuestUserContext.cpp | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 871866cb31..3f2cf5b439 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -109,6 +109,16 @@ asm(".text\n" " popq %rsi\n" " popq %rdi\n" "2:\n" +#ifdef _WIN32 + " pushq %rdi\n" + " pushq %rsi\n" + " movq 16(%rdi), %rdi\n" + " subq $8, %rsp\n" + " call GuestStackSwitch_nid_no_patch\n" + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" +#endif " movq %rdi, %rdx\n" " movl %esi, %eax\n" " movq 0(%rdx), %rcx\n" diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 41a4b5e0b9..cd05dca097 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -21,6 +21,8 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context); void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); [[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); [[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); +[[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); +[[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int* APS5_VABI __error_nid_postfix(); @@ -150,6 +152,7 @@ int g_entries = 0; Fibre g_dispatcher = {}; Fibre g_job = {}; int g_jobRuns = 0; +bool g_signalJumps = false; void Require(bool value, const char* what) { if (!value) { @@ -348,6 +351,14 @@ namespace { [[gnu::noinline]] void Switch(Fibre* from, Fibre* to) { from->started = 1; + if (g_signalJumps) { + if (sigsetjmp_nid_postfix(from->jump, 1) == 0) { + if (to->started) siglongjmp_nid_postfix(to->jump, 1); + setcontext_nid_postfix(&to->context); + Require(false, "setcontext into the job returned"); + } + return; + } if (setjmp_nid_postfix(from->jump) == 0) { if (to->started) longjmp_nid_postfix(to->jump, 1); setcontext_nid_postfix(&to->context); @@ -360,7 +371,7 @@ namespace { extern "C" void APS5_VABI JobEntry() { for (;;) { ++g_jobRuns; - CheckStackBounds(&g_jobStack, "the stack bounds follow longjmp onto the job stack"); + CheckStackBounds(&g_jobStack, g_signalJumps ? "the stack bounds follow siglongjmp onto the job stack" : "the stack bounds follow longjmp onto the job stack"); ThrowAndCatch(); Switch(&g_job, &g_dispatcher); } @@ -402,7 +413,11 @@ void CheckMakecontext() { #endif } -void CheckJumpBuffers() { +void CheckJumpBuffers(bool signalJumps) { + g_signalJumps = signalJumps; + g_jobRuns = 0; + g_dispatcher = {}; + g_job = {}; #ifdef _WIN32 ULONG_PTR threadLow = 0; ULONG_PTR threadHigh = 0; @@ -420,7 +435,7 @@ void CheckJumpBuffers() { ULONG_PTR low = 0; ULONG_PTR high = 0; GetCurrentThreadStackLimits(&low, &high); - Require(low == threadLow && high == threadHigh, "the stack bounds follow longjmp back to the thread stack"); + Require(low == threadLow && high == threadHigh, signalJumps ? "the stack bounds follow siglongjmp back to the thread stack" : "the stack bounds follow longjmp back to the thread stack"); #endif } } @@ -466,7 +481,8 @@ int main(int argc, char** argv) { CheckSignalMask(); std::thread([] { CheckMakecontext(); - CheckJumpBuffers(); + CheckJumpBuffers(false); + CheckJumpBuffers(true); GrowStack(32); }).join(); return 0; From b0c0e1dadff8764666e2dbd7291c565cb1c36236 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 17:06:14 +0300 Subject: [PATCH 126/353] fix(libc): return null with ENOMEM or EINVAL from malloc, calloc, realloc, memalign and aligned_alloc A size the default heap rejects or the guest arena cannot place, a calloc product that overflows, and an invalid alignment threw through the guest call, which aborted the process. FreeBSD 11.4 returns null with ENOMEM, or EINVAL for an alignment that is not a power of two. Guest arena exhaustion now throws bad_alloc. --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/FileAndHeap.cpp | 33 +++++- core/libs/prx/libc/src/GuestArena.cpp | 3 +- core/libs/tests/GuestAllocatorFailures.cpp | 123 +++++++++++++++++++++ core/libs/tests/GuestHeapRounding.cpp | 17 +-- docs/dev/debt/libc.md | 3 +- 6 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 core/libs/tests/GuestAllocatorFailures.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 8f2342a25b..5eaaaf20e1 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -584,6 +584,15 @@ if(BUILD_TESTING) set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_allocator_failures_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatorFailures.cpp) + target_include_directories(guest_allocator_failures_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocator_failures_tests PRIVATE libc) + configure_windows_unwind(guest_allocator_failures_tests) + add_test(NAME guest_allocator_failures COMMAND guest_allocator_failures_tests) + if(WIN32) + set_tests_properties(guest_allocator_failures PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(application_heap_stats_tests ${LIBS_INCLUDE_DIR}/tests/ApplicationHeapStats.cpp) target_include_directories(application_heap_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_stats_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index ef219f06bb..d3ee434e7c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -210,6 +210,25 @@ static void SortElements(unsigned char* base, size_t count, size_t size, GuestCo } } +namespace { + +template +void* allocationOrNull(TAllocate allocate) { + try { + return allocate(); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + errno = 12; + return nullptr; +} + +bool validAlignment(std::size_t alignment) { + return alignment != 0 && (alignment & (alignment - 1)) == 0; +} + +} + extern "C" { [[noreturn]] void APS5_VABI _ZSt11_Xbad_allocv_nid_postfix(); @@ -451,7 +470,7 @@ size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { } void* APS5_VABI malloc_nid_postfix(size_t size) { - return ApplicationHeapAllocate_nid_no_patch(size); + return allocationOrNull([size] { return ApplicationHeapAllocate_nid_no_patch(size); }); } void APS5_VABI free_nid_postfix(void* ptr) { @@ -459,7 +478,7 @@ void APS5_VABI free_nid_postfix(void* ptr) { } void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { - return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + return allocationOrNull([ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { @@ -474,11 +493,15 @@ void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + if (!validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); } void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + return memalign_nid_postfix(alignment, size); } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { @@ -486,7 +509,7 @@ void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignmen } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { - return ApplicationHeapCalloc_nid_no_patch(count, size); + return allocationOrNull([count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }); } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5ff89ebc9c..09935f968f 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -79,7 +80,7 @@ public: if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; candidate = std::max(candidate, alignUp(end, alignment)); } - if (candidate > _end || bytes > _end - candidate) throw std::runtime_error("guest address space arena exhausted"); + if (candidate > _end || bytes > _end - candidate) throw std::bad_alloc(); _used.emplace(candidate, candidate + bytes); return reinterpret_cast(candidate); } diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp new file mode 100644 index 0000000000..4062d81ac4 --- /dev/null +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -0,0 +1,123 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t); +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI aligned_alloc_nid_postfix(std::size_t, std::size_t); +int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int NoMemory = 12; +constexpr int InvalidArgument = 22; +constexpr std::size_t Max = std::numeric_limits::max(); +constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits::max()) + 1; + +int failures = 0; + +void report(const char* what, const char* detail) { + std::fprintf(stderr, "%s: %s\n", what, detail); + ++failures; +} + +template +void expectNull(const char* what, int expectedError, TCall call) { + *__error_nid_postfix() = 0; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s instead of returning NULL\n", what, error.what()); + ++failures; + return; + } catch (...) { + report(what, "threw a non-standard exception instead of returning NULL"); + return; + } + if (result != nullptr) { + report(what, "returned a block"); + free_nid_postfix(result); + return; + } + if (*__error_nid_postfix() != expectedError) { + std::fprintf(stderr, "%s: errno %d, expected %d\n", what, *__error_nid_postfix(), expectedError); + ++failures; + } +} + +template +void* expectBlock(const char* what, TCall call) { + *__error_nid_postfix() = 77; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s\n", what, error.what()); + ++failures; + return nullptr; + } + if (result == nullptr) report(what, "returned NULL"); + if (*__error_nid_postfix() != 77) report(what, "changed errno on success"); + return result; +} + +} + +int main() { + void* api[10]{}; + ApplicationHeapRegister_nid_no_patch(api); + + expectNull("malloc(SIZE_MAX)", NoMemory, [] { return malloc_nid_postfix(Max); }); + expectNull("malloc(PTRDIFF_MAX + 1)", NoMemory, [] { return malloc_nid_postfix(HalfPlusOne); }); + expectNull("calloc(SIZE_MAX, 2)", NoMemory, [] { return calloc_nid_postfix(Max, 2); }); + expectNull("calloc(2^33, 2^33)", NoMemory, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); + expectNull("calloc(PTRDIFF_MAX + 1, 1)", NoMemory, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); + expectNull("realloc(NULL, SIZE_MAX)", NoMemory, [] { return realloc_nid_postfix(nullptr, Max); }); + expectNull("aligned_alloc(64, SIZE_MAX)", NoMemory, [] { return aligned_alloc_nid_postfix(64, Max); }); + expectNull("memalign(64, PTRDIFF_MAX + 1)", NoMemory, [] { return memalign_nid_postfix(64, HalfPlusOne); }); + expectNull("aligned_alloc(24, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(24, 64); }); + expectNull("aligned_alloc(0, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(0, 64); }); + expectNull("memalign(24, 64)", InvalidArgument, [] { return memalign_nid_postfix(24, 64); }); + + auto* block = static_cast(expectBlock("malloc(40)", [] { return malloc_nid_postfix(40); })); + if (block != nullptr) { + std::memset(block, 0x6b, 40); + expectNull("realloc(block, SIZE_MAX)", NoMemory, [block] { return realloc_nid_postfix(block, Max); }); + expectNull("realloc(block, PTRDIFF_MAX + 1)", NoMemory, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); + for (unsigned index = 0; index < 40; ++index) { + if (block[index] != 0x6b) report("realloc failure", "damaged the original block"); + } + free_nid_postfix(block); + } + + void* untouched = &failures; + *__error_nid_postfix() = 77; + if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, HalfPlusOne) != NoMemory) report("posix_memalign(64, PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, Max) != NoMemory) report("posix_memalign(64, SIZE_MAX)", "did not return ENOMEM"); + if (untouched != &failures || *__error_nid_postfix() != 77) report("posix_memalign failure", "touched the output or errno"); + + free_nid_postfix(expectBlock("calloc(3, 5)", [] { return calloc_nid_postfix(3, 5); })); + free_nid_postfix(expectBlock("calloc(0, 5)", [] { return calloc_nid_postfix(0, 5); })); + free_nid_postfix(expectBlock("aligned_alloc(128, 1)", [] { return aligned_alloc_nid_postfix(128, 1); })); + free_nid_postfix(expectBlock("memalign(4096, 1)", [] { return memalign_nid_postfix(4096, 1); })); + void* first = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + void* second = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + if (first != nullptr && first == second) report("malloc(0)", "returned the same block twice"); + free_nid_postfix(first); + free_nid_postfix(second); + + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestHeapRounding.cpp b/core/libs/tests/GuestHeapRounding.cpp index 1fd9527a6d..d888f7311f 100644 --- a/core/libs/tests/GuestHeapRounding.cpp +++ b/core/libs/tests/GuestHeapRounding.cpp @@ -20,6 +20,7 @@ extern "C" { void* APS5_VABI malloc_nid_postfix(std::size_t); void APS5_VABI free_nid_postfix(void*); void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -76,13 +77,14 @@ DWORD runChild(const wchar_t* mode) { int runMallocOverflow() { registerDefaultHeap(); + *__error_nid_postfix() = 0; try { auto* pointer = malloc_nid_postfix(std::numeric_limits::max() - HeaderBytes); - free_nid_postfix(pointer); - return 1; - } catch (const std::length_error& error) { - std::fprintf(stderr, "malloc overflow rejected: %s\n", error.what()); - return 0; + if (pointer != nullptr) { + free_nid_postfix(pointer); + return 1; + } + return *__error_nid_postfix() == 12 ? 0 : 4; } catch (const std::exception& error) { std::fprintf(stderr, "malloc failed with an unexpected exception: %s\n", error.what()); return 2; @@ -98,15 +100,14 @@ int runReallocOverflow() { auto* original = static_cast(malloc_nid_postfix(bytes)); std::memset(original, 0x5a, bytes); bool rejectedWithExpectedError = false; + *__error_nid_postfix() = 0; try { auto* result = realloc_nid_postfix(original, std::numeric_limits::max() - HeaderBytes); if (result != nullptr) { free_nid_postfix(result); return 4; } - } catch (const std::length_error& error) { - rejectedWithExpectedError = true; - std::fprintf(stderr, "realloc overflow rejected: %s\n", error.what()); + rejectedWithExpectedError = *__error_nid_postfix() == 12; } catch (const std::exception& error) { std::fprintf(stderr, "realloc failed with an unexpected exception: %s\n", error.what()); } catch (...) { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad..71cf4643ae 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -11,7 +11,8 @@ - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (an exhausted guest arena, an unknown pointer) throw, as they do for `realloc` +- [malloc, calloc, realloc, memalign and aligned_alloc](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. When the host refuses to commit memory for a block that fits in the arena (the paging file or commit limit is exhausted) the allocation still throws. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (the host refusing to commit memory inside the guest arena, an unknown pointer) throw, as they do for `realloc` - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol From c520fda39b404f78c556b06369722d99e206f6f0 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 17:59:36 +0300 Subject: [PATCH 127/353] fix(libc): return ENOMEM when the host cannot commit a heap block and from reallocalign A commit failure inside the guest arena threw and left the reserved range in use. The heap now resets and releases the range, and returns null with ENOMEM when the host is out of memory. reallocalign returns null with EINVAL or ENOMEM like the other allocator entry points. --- core/libs/prx/libc/src/FileAndHeap.cpp | 6 ++++- core/libs/prx/libc/src/GuestHeap.cpp | 28 +++++++++++++++++++++- core/libs/tests/GuestAllocatorFailures.cpp | 28 ++++++++++++++++++++++ core/libs/tests/GuestReallocalign.cpp | 18 +++++++------- docs/dev/debt/libc.md | 4 ++-- 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d3ee434e7c..472612acf7 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -505,7 +505,11 @@ void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { - return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); + if (size != 0 && !validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index b2ae0d13ae..de5d8b27b2 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -112,7 +112,33 @@ private: } static void commit(void* pointer, std::size_t bytes) { - GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); + try { + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); + } catch (const std::system_error& error) { + abandon(pointer, bytes); + if (outOfMemory(error)) throw std::bad_alloc(); + throw; + } catch (...) { + abandon(pointer, bytes); + throw; + } + } + + static void abandon(void* pointer, std::size_t bytes) { + GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); + GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); + } + + static bool outOfMemory(const std::system_error& error) { +#ifdef _WIN32 + constexpr int NotEnoughMemory = 8; + constexpr int OutOfMemory = 14; + constexpr int CommitmentLimit = 1455; + return error.code().category() == std::system_category() && + (error.code().value() == NotEnoughMemory || error.code().value() == OutOfMemory || error.code().value() == CommitmentLimit); +#else + return error.code() == std::errc::not_enough_memory; +#endif } // Freed large blocks stay committed and are handed out again for the same block size: the game diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 4062d81ac4..cec6aafe3c 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -102,6 +103,33 @@ int main() { free_nid_postfix(block); } + if (GuestArena::GuestArenaAvailable_nid_postfix()) { + constexpr std::size_t beyondCommitLimit = std::size_t{300} << 30; + const auto freeBefore = GuestArena::GuestArenaHighestFree_nid_no_patch(beyondCommitLimit, 0x4000); + *__error_nid_postfix() = 0; + void* huge = nullptr; + try { + huge = malloc_nid_postfix(beyondCommitLimit); + } catch (const std::exception& error) { + std::fprintf(stderr, "malloc(300 GiB): threw %s\n", error.what()); + ++failures; + } + if (huge != nullptr) { + free_nid_postfix(huge); + } else if (*__error_nid_postfix() != NoMemory && failures == 0) { + report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } + if (huge == nullptr && GuestArena::GuestArenaHighestFree_nid_no_patch(beyondCommitLimit, 0x4000) != freeBefore) { + report("malloc(300 GiB)", "left its arena range reserved after failing"); + } + void* small = malloc_nid_postfix(100); + if (small == nullptr) report("malloc(100) after a failed huge malloc", "returned NULL"); + free_nid_postfix(small); + void* large = malloc_nid_postfix(std::size_t{1} << 20); + if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); + free_nid_postfix(large); + } + void* untouched = &failures; *__error_nid_postfix() = 77; if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index bb66c3979c..fded34f311 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { void* APS5_VABI reallocalign_nid_postfix(void*, std::size_t, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -14,13 +16,6 @@ void Require(bool value) { if (!value) throw std::runtime_error("reallocalign check failed"); } -template -void Reject(TAction action) { - bool rejected = false; - try { action(); } catch (const std::exception&) { rejected = true; } - Require(rejected); -} - } int main() { @@ -35,7 +30,12 @@ int main() { auto* shrunk = static_cast(reallocalign_nid_postfix(grown, 16, 16)); Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 16) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 0); }); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 3); }); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 0) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 3) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, std::numeric_limits::max(), 16) == nullptr && *__error_nid_postfix() == 12); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); Require(reallocalign_nid_postfix(shrunk, 0, 16) == nullptr); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 71cf4643ae..2156a34328 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -11,8 +11,8 @@ - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` -- [malloc, calloc, realloc, memalign and aligned_alloc](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. When the host refuses to commit memory for a block that fits in the arena (the paging file or commit limit is exhausted) the allocation still throws. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (the host refusing to commit memory inside the guest arena, an unknown pointer) throw, as they do for `realloc` +- [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol From 39f6ac634249ae881790572ea9a0083605359cb6 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 18:10:44 +0300 Subject: [PATCH 128/353] fix(libc): report guest arena exhaustion from the heap as out of memory The guest heap turns an exhausted arena into bad_alloc itself instead of changing the arena's exception. --- core/libs/prx/libc/src/GuestArena.cpp | 3 +-- core/libs/prx/libc/src/GuestHeap.cpp | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 09935f968f..5ff89ebc9c 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -80,7 +79,7 @@ public: if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; candidate = std::max(candidate, alignUp(end, alignment)); } - if (candidate > _end || bytes > _end - candidate) throw std::bad_alloc(); + if (candidate > _end || bytes > _end - candidate) throw std::runtime_error("guest address space arena exhausted"); _used.emplace(candidate, candidate + bytes); return reinterpret_cast(candidate); } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index de5d8b27b2..d35ccdb451 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -52,7 +52,7 @@ public: if (total > MaximumSmallBytes) { blockBytes = alignUp(total, LargeGranuleBytes); if (void* cached = _large.Take(blockBytes)) return cached; - void* raw = GuestArena::GuestArenaAllocate_nid_postfix(blockBytes, PageBytes); + void* raw = arenaAllocate(blockBytes, PageBytes); commit(raw, blockBytes); return raw; } @@ -65,7 +65,7 @@ public: return block; } if (sizeClass.bump == nullptr || sizeClass.bytes > static_cast(sizeClass.bumpEnd - sizeClass.bump)) { - auto* span = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SpanBytes, PageBytes)); + auto* span = static_cast(arenaAllocate(SpanBytes, PageBytes)); commit(span, SpanBytes); sizeClass.bump = span; sizeClass.bumpEnd = span + SpanBytes; @@ -111,6 +111,14 @@ private: return static_cast(found - _classes.begin()); } + static void* arenaAllocate(std::size_t bytes, std::size_t alignment) { + try { + return GuestArena::GuestArenaAllocate_nid_postfix(bytes, alignment); + } catch (const std::runtime_error&) { + throw std::bad_alloc(); + } + } + static void commit(void* pointer, std::size_t bytes) { try { GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); From 177324e3e93126987490ef97a17f66784800cecc Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 18:30:49 +0300 Subject: [PATCH 129/353] test(libc): detect an arena range leaked by a failed heap commit --- core/libs/tests/GuestAllocatorFailures.cpp | 41 ++++++++++++++-------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index cec6aafe3c..abd4bd122d 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -105,22 +105,33 @@ int main() { if (GuestArena::GuestArenaAvailable_nid_postfix()) { constexpr std::size_t beyondCommitLimit = std::size_t{300} << 30; - const auto freeBefore = GuestArena::GuestArenaHighestFree_nid_no_patch(beyondCommitLimit, 0x4000); - *__error_nid_postfix() = 0; - void* huge = nullptr; - try { - huge = malloc_nid_postfix(beyondCommitLimit); - } catch (const std::exception& error) { - std::fprintf(stderr, "malloc(300 GiB): threw %s\n", error.what()); - ++failures; + constexpr std::size_t probeBytes = std::size_t{100} << 30; + void* before = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(before, probeBytes); + bool committed = false; + for (int attempt = 0; attempt < 10 && !committed; ++attempt) { + *__error_nid_postfix() = 0; + void* huge = nullptr; + try { + huge = malloc_nid_postfix(beyondCommitLimit); + } catch (const std::exception& error) { + std::fprintf(stderr, "malloc(300 GiB): threw %s\n", error.what()); + ++failures; + break; + } + if (huge != nullptr) { + free_nid_postfix(huge); + committed = true; + } else if (*__error_nid_postfix() != NoMemory) { + report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } } - if (huge != nullptr) { - free_nid_postfix(huge); - } else if (*__error_nid_postfix() != NoMemory && failures == 0) { - report("malloc(300 GiB)", "returned NULL without ENOMEM"); - } - if (huge == nullptr && GuestArena::GuestArenaHighestFree_nid_no_patch(beyondCommitLimit, 0x4000) != freeBefore) { - report("malloc(300 GiB)", "left its arena range reserved after failing"); + if (committed) { + std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); + } else { + void* after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(after, probeBytes); + if (after != before) report("malloc(300 GiB)", "left its arena range reserved after failing"); } void* small = malloc_nid_postfix(100); if (small == nullptr) report("malloc(100) after a failed huge malloc", "returned NULL"); From 61ed25d9ed3033a04e7ccd7c16bccb61dffb41e7 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:13:12 +0200 Subject: [PATCH 130/353] fix(libkernel): drop the cached unwind tables when the loader frees a module The cache key of relinked images has no time stamp or check sum, so a different module of the same size loaded at the same base was served the old tables. The loader now forgets the image when it frees the module, the cache has a separate valid flag, and the sysmodule name cache is left out of this change. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 52 ++----------------- .../libkernel/Module/src/DynamicLoader.cpp | 5 ++ core/libs/prx/libkernel/Module/src/Module.cpp | 37 +++++++------ core/libs/prx/libkernel/System/src/Rtld.cpp | 8 ++- core/libs/tests/GuestUnwindModuleInfo.cpp | 41 +++++++++++++++ .../tests/GuestUnwindModuleInfoFixture.cpp | 2 + .../tests/GuestUnwindModuleInfoFixture.hpp | 2 + docs/dev/debt/libkernel.md | 2 +- 8 files changed, 85 insertions(+), 64 deletions(-) diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 7d3a322154..8ed9239477 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -38,52 +38,6 @@ void* openHostModule(const char* name) { return dlopen_nid_postfix(relative.c_str(), kRtldNow); } -#ifdef _WIN32 -struct ImageName { - DWORD stamp = 0; - DWORD checksum = 0; - DWORD size = 0; - std::string name; -}; - -std::string queryMappedFileName(void* address) { - char path[4096] = {}; - const DWORD length = GetMappedFileNameA(GetCurrentProcess(), address, path, sizeof(path) - 1); - return std::string(path, length); -} - -void copyMappedFileName(const MEMORY_BASIC_INFORMATION& mbi, ModuleInfoForUnwind* info) { - std::string name; - if (mbi.Type != MEM_IMAGE) { - name = queryMappedFileName(mbi.BaseAddress); - } else { - const auto* base = static_cast(mbi.AllocationBase); - const auto* dos = reinterpret_cast(base); - const auto* nt = dos->e_magic == IMAGE_DOS_SIGNATURE ? reinterpret_cast(base + dos->e_lfanew) : nullptr; - if (nt == nullptr || nt->Signature != IMAGE_NT_SIGNATURE) { - name = queryMappedFileName(mbi.BaseAddress); - } else { - static std::mutex mutex; - static auto& names = *new std::unordered_map(); - std::lock_guard lock(mutex); - auto& cached = names[mbi.AllocationBase]; - if (cached.name.empty() || cached.stamp != nt->FileHeader.TimeDateStamp || cached.checksum != nt->OptionalHeader.CheckSum || - cached.size != nt->OptionalHeader.SizeOfImage) { - cached.stamp = nt->FileHeader.TimeDateStamp; - cached.checksum = nt->OptionalHeader.CheckSum; - cached.size = nt->OptionalHeader.SizeOfImage; - cached.name = queryMappedFileName(mbi.BaseAddress); - } - std::strncpy(info->name, cached.name.c_str(), sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; - return; - } - } - std::strncpy(info->name, name.c_str(), sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; -} -#endif - bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { #ifdef _WIN32 MEMORY_BASIC_INFORMATION mbi{}; @@ -96,7 +50,11 @@ bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { info->eh_frame_size = 0; info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; - copyMappedFileName(mbi, info); + char path[4096] = {}; + DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); + path[len] = '\0'; + std::strncpy(info->name, path, sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; return true; #else const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 1dc372c387..b66319ac67 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -24,6 +24,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnwindImage_nid_no_patch(const void* base); +#endif + namespace { thread_local std::array loaderError{}; thread_local bool pendingError = false; @@ -39,6 +43,7 @@ struct Module { if (owned && native) { #ifdef _WIN32 FreeLibrary(static_cast(native)); + ForgetUnwindImage_nid_no_patch(native); #else ::dlclose(native); #endif diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 907b99578e..63a01ca89e 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -3,8 +3,6 @@ #include #include #include -#include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" @@ -13,6 +11,8 @@ #include "prx/libkernel/Module/EhFrame.hpp" #include "prx/libkernel/Module/ModuleArgs.hpp" #ifdef _WIN32 +#include +#include #include #include #elif defined(__APPLE__) @@ -27,8 +27,7 @@ #ifdef _WIN32 namespace { struct UnwindImage { - DWORD stamp = 0; - DWORD checksum = 0; + bool filled = false; DWORD size = 0; bool hasTables = false; EhFrame::Tables tables; @@ -37,28 +36,30 @@ struct UnwindImage { std::string name; }; +std::mutex unwindImagesMutex; +std::unordered_map& UnwindImages() { + static auto& images = *new std::unordered_map(); + return images; +} + std::uint32_t ReadWord(std::uint64_t address) { std::uint32_t value; std::memcpy(&value, reinterpret_cast(address), sizeof(value)); return value; } -void FillUnwindImage(const std::uint8_t* base, ModuleInfoForUnwind* info) { +void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { + const auto* base = static_cast(allocationBase); const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); - static std::mutex mutex; - static auto& images = *new std::unordered_map(); - std::lock_guard lock(mutex); - auto& image = images[reinterpret_cast(base)]; - const bool current = !image.name.empty() && image.stamp == nt->FileHeader.TimeDateStamp && image.checksum == nt->OptionalHeader.CheckSum && - image.size == nt->OptionalHeader.SizeOfImage && + std::lock_guard lock(unwindImagesMutex); + auto& image = UnwindImages()[reinterpret_cast(base)]; + const bool current = image.filled && image.size == nt->OptionalHeader.SizeOfImage && (!image.hasTables || (ReadWord(image.tables.header) == image.headerWord && ReadWord(image.tables.frames) == image.framesWord)); if (!current) { UnwindImage fresh; - fresh.stamp = nt->FileHeader.TimeDateStamp; - fresh.checksum = nt->OptionalHeader.CheckSum; fresh.size = nt->OptionalHeader.SizeOfImage; fresh.hasTables = EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); if (fresh.hasTables) { @@ -66,8 +67,9 @@ void FillUnwindImage(const std::uint8_t* base, ModuleInfoForUnwind* info) { fresh.framesWord = ReadWord(fresh.tables.frames); } char path[4096] = {}; - const DWORD length = GetMappedFileNameA(GetCurrentProcess(), const_cast(base), path, sizeof(path) - 1); + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); fresh.name.assign(path, length); + fresh.filled = true; image = std::move(fresh); } if (image.hasTables) { @@ -81,6 +83,11 @@ void FillUnwindImage(const std::uint8_t* base, ModuleInfoForUnwind* info) { info->name[sizeof(info->name) - 1] = '\0'; } } + +extern "C" void ForgetUnwindImage_nid_no_patch(const void* base) { + std::lock_guard lock(unwindImagesMutex); + UnwindImages().erase(reinterpret_cast(base)); +} #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); @@ -157,7 +164,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; if (mbi.Type == MEM_IMAGE) { - FillUnwindImage(static_cast(mbi.AllocationBase), info); + FillUnwindImage(mbi.AllocationBase, info); return 0; } char path[4096] = {}; diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index bcf6eff7f7..5a4977754f 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -19,6 +19,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnwindImage_nid_no_patch(const void* base); +#endif + namespace { std::mutex rtldHeapMutex; @@ -69,7 +73,9 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { } if (!handle) return 0; #ifdef _WIN32 - if (!FreeLibrary(static_cast(handle))) + const bool released = FreeLibrary(static_cast(handle)); + ForgetUnwindImage_nid_no_patch(handle); + if (!released) #else if (dlclose(handle) != 0) #endif diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index 31d92cd007..bab2a0752f 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -3,11 +3,15 @@ #include #include #include +#include #include #include #include extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); +extern "C" void* APS5_VABI dlopen_nid_postfix(const char*, int); +extern "C" void* APS5_VABI dlsym_nid_postfix(void*, const char*); +extern "C" int APS5_VABI dlclose_nid_postfix(void*); static void Require(bool value) { if (!value) std::abort(); } @@ -21,6 +25,41 @@ static bool Throws(std::uint64_t address) { return false; } +static void ReloadedModuleIsRead() { + HMODULE linked = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(GetUnwindFixture()), &linked)); + wchar_t original[MAX_PATH] = {}; + Require(GetModuleFileNameW(linked, original, MAX_PATH) != 0); + const auto copy = std::filesystem::path(original).replace_filename("unwind_fixture_reload.dll"); + std::filesystem::copy_file(original, copy, std::filesystem::copy_options::overwrite_existing); + const auto path = copy.string(); + + std::uint64_t firstBase = 0; + for (int round = 0; round < 2; ++round) { + void* handle = dlopen_nid_postfix(path.c_str(), 2); + Require(handle != nullptr); + const auto get = reinterpret_cast(dlsym_nid_postfix(handle, "GetUnwindFixture_nid_no_patch")); + Require(get != nullptr); + auto* fixture = get(); + const auto address = reinterpret_cast(fixture); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != 0); + const auto base = reinterpret_cast(memory.AllocationBase); + if (round == 0) firstBase = base; + else Require(base == firstBase); + if (round == 0) { + fixture->frames.terminator = 12; + fixture->frames.spare[0] = 1; + } + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(address, 0, &info) == 0); + Require(info.eh_frame_size == (round == 0 ? 4 + 12 + 4 + 12 : 4 + 12)); + Require(dlclose_nid_postfix(handle) == 0); + } + std::filesystem::remove(copy); +} + int main() { auto* fixture = GetUnwindFixture(); Require(fixture != nullptr); @@ -62,6 +101,8 @@ int main() { fixture->frames.cieLength = 12; Require(!Throws(address)); + ReloadedModuleIsRead(); + ModuleInfoForUnwind host{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &host) == 0); Require(host.eh_frame_hdr_addr == 0 && host.eh_frame_addr == 0 && host.eh_frame_size == 0); diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp index 2e473b4bff..a5abebb36c 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp @@ -21,3 +21,5 @@ extern "C" UnwindFixture* GetUnwindFixture() { UnwindFixtureMeta = static_cast(reinterpret_cast(&fixture.header) - base); return &fixture; } + +extern "C" UnwindFixture* GetUnwindFixture_nid_no_patch() { return GetUnwindFixture(); } diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp index d03be890d2..08b2e941d6 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp @@ -16,6 +16,8 @@ struct alignas(4) UnwindFixtureFrames { std::uint32_t cieId = 0; std::uint8_t cie[8] = {1, 0, 1, 0x78, 16, 0, 0, 0}; std::uint32_t terminator = 0; + std::uint32_t spare[3] = {}; + std::uint32_t spareTerminator = 0; }; struct UnwindFixture { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 59fff7e1d0..abf412acd5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,7 +30,7 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows -- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp), [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libkernel, libSceSysmodule) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base, keyed with the image's time stamp, check sum and size. A cached entry is reused only while the `eh_frame_hdr` header word and the first `eh_frame` length word still match, so a change deeper in the tables, or a different image with the same base, time stamp, check sum and size, is not noticed. The `.eh_frame` walk that finds its end is done once per image instead of once per call +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped when the loader frees the module (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release). A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it From 8e97ef3564215ad8ed1987fa476adfcb3dbed287 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Tue, 13 Oct 2026 22:14:28 +0600 Subject: [PATCH 131/353] test(libc): exercise failed heap commits under a Linux data limit --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/tests/GuestAllocatorFailures.cpp | 39 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 5eaaaf20e1..64bc46be55 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -589,6 +589,7 @@ if(BUILD_TESTING) target_link_libraries(guest_allocator_failures_tests PRIVATE libc) configure_windows_unwind(guest_allocator_failures_tests) add_test(NAME guest_allocator_failures COMMAND guest_allocator_failures_tests) + set_tests_properties(guest_allocator_failures PROPERTIES TIMEOUT 20) if(WIN32) set_tests_properties(guest_allocator_failures PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index abd4bd122d..22629e0166 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -1,12 +1,16 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include #include #include +#ifdef __linux__ +#include +#endif extern "C" { void* APS5_VABI malloc_nid_postfix(std::size_t); @@ -104,11 +108,25 @@ int main() { } if (GuestArena::GuestArenaAvailable_nid_postfix()) { +#ifdef __linux__ + rlimit originalLimit{}; + if (getrlimit(RLIMIT_DATA, &originalLimit) != 0) { + report("failed commit setup", "could not read RLIMIT_DATA"); + return 1; + } + auto limited = originalLimit; + limited.rlim_cur = std::min(limited.rlim_cur, rlim_t{128} << 20); + if (setrlimit(RLIMIT_DATA, &limited) != 0) { + report("failed commit setup", "could not limit RLIMIT_DATA"); + return 1; + } +#endif constexpr std::size_t beyondCommitLimit = std::size_t{300} << 30; constexpr std::size_t probeBytes = std::size_t{100} << 30; void* before = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); GuestArena::GuestArenaRelease_nid_postfix(before, probeBytes); bool committed = false; + int failedCommits = 0; for (int attempt = 0; attempt < 10 && !committed; ++attempt) { *__error_nid_postfix() = 0; void* huge = nullptr; @@ -124,14 +142,31 @@ int main() { committed = true; } else if (*__error_nid_postfix() != NoMemory) { report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } else { + ++failedCommits; } } +#ifdef __linux__ + if (setrlimit(RLIMIT_DATA, &originalLimit) != 0) report("failed commit cleanup", "could not restore RLIMIT_DATA"); +#endif if (committed) { +#ifdef __linux__ + report("malloc(300 GiB)", "succeeded despite RLIMIT_DATA"); +#else std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); +#endif } else { - void* after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); - GuestArena::GuestArenaRelease_nid_postfix(after, probeBytes); + if (failedCommits != 10) report("failed heap commits", "did not return ENOMEM for all ten attempts"); + void* after = nullptr; + try { + after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(after, probeBytes); + } catch (const std::exception& error) { + std::fprintf(stderr, "failed commit range probe: %s\n", error.what()); + ++failures; + } if (after != before) report("malloc(300 GiB)", "left its arena range reserved after failing"); + std::fprintf(stderr, "Checked %d failed heap commits and arena range reuse\n", failedCommits); } void* small = malloc_nid_postfix(100); if (small == nullptr) report("malloc(100) after a failed huge malloc", "returned NULL"); From 5c802bbf1014993a6a5825bd4498df07c0c45ea1 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:01:53 +0200 Subject: [PATCH 132/353] refactor(recompiler): split the ShaderInfo encoder and the entry envelope encodeInfo and decodeInfo wrote the ShaderInfo fields inline between the compiled-shader header and the binding layout, and EncodeEntry and DecodeEntry built and checked the file header (magic, format, source version, key and payload hashes) inline. Move both into helpers, encodeShaderInfo/decodeShaderInfo and wrapEntry/unwrapEntry, so that a second kind of entry can reuse them. The bytes of a compiled variant entry do not change. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/ShaderDiskCache.cpp | 89 +++++++++++++--------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index f25dc30969..713bff2c8b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -452,15 +452,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { }); } -void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { - writer.Value(compiled.stage); - writer.Value(compiled.shaderHash); - writer.Value(compiled.waveSize); - writer.Value(compiled.userDataBase); - writer.Value(compiled.userDataCount); - writer.Value(compiled.scratchDwords); - writer.Value(compiled.paramExportMask); - const auto& info = compiled.info; +void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.scratchDwords); writer.Value(info.sharedMemoryBytes); writer.List(info.buffers, [](Writer& out, const BufferResource& buffer) { @@ -558,18 +550,9 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.dispatchThreadLimit); writer.Value(info.rasterPrimitive); writer.Value(info.layerExported); - encodeLayout(writer, compiled.bindings); } -void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { - reader.Value(compiled.stage); - reader.Value(compiled.shaderHash); - reader.Value(compiled.waveSize); - reader.Value(compiled.userDataBase); - reader.Value(compiled.userDataCount); - reader.Value(compiled.scratchDwords); - reader.Value(compiled.paramExportMask); - auto& info = compiled.info; +void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.scratchDwords); reader.Value(info.sharedMemoryBytes); reader.List(info.buffers, 27, [](Reader& in, BufferResource& buffer) { @@ -669,6 +652,29 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.dispatchThreadLimit); reader.Value(info.rasterPrimitive); reader.Value(info.layerExported); +} + +void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { + writer.Value(compiled.stage); + writer.Value(compiled.shaderHash); + writer.Value(compiled.waveSize); + writer.Value(compiled.userDataBase); + writer.Value(compiled.userDataCount); + writer.Value(compiled.scratchDwords); + writer.Value(compiled.paramExportMask); + encodeShaderInfo(writer, compiled.info); + encodeLayout(writer, compiled.bindings); +} + +void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { + reader.Value(compiled.stage); + reader.Value(compiled.shaderHash); + reader.Value(compiled.waveSize); + reader.Value(compiled.userDataBase); + reader.Value(compiled.userDataCount); + reader.Value(compiled.scratchDwords); + reader.Value(compiled.paramExportMask); + decodeShaderInfo(reader, compiled.info); decodeLayout(reader, compiled.bindings); } @@ -717,6 +723,30 @@ std::string hex(std::uint64_t value) { return text; } +std::vector wrapEntry(std::span key, std::span payload) { + const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; + std::vector file(sizeof(header) + key.size() + payload.size()); + std::memcpy(file.data(), &header, sizeof(header)); + std::memcpy(file.data() + sizeof(header), key.data(), key.size()); + if (!payload.empty()) std::memcpy(file.data() + sizeof(header) + key.size(), payload.data(), payload.size()); + return file; +} + +LoadStatus unwrapEntry(std::span file, std::span key, std::span& payload) { + if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; + FileHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; + const auto body = file.size() - sizeof(header); + if (header.keyBytes > body || header.payloadBytes != body - header.keyBytes) return LoadStatus::Rejected; + const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); + payload = file.subspan(sizeof(header) + static_cast(header.keyBytes)); + if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; + if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; + if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; + return LoadStatus::Loaded; +} + class DiskStore { public: DiskStore() { @@ -950,26 +980,13 @@ std::vector EncodeEntry(std::span key, const Compile encodeArtifact(writer, variant.artifact); encodeInfo(writer, variant.info); encodeAllocation(writer, variant.bindings); - const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; - std::vector file(sizeof(header) + key.size() + payload.size()); - std::memcpy(file.data(), &header, sizeof(header)); - std::memcpy(file.data() + sizeof(header), key.data(), key.size()); - std::memcpy(file.data() + sizeof(header) + key.size(), payload.data(), payload.size()); - return file; + return wrapEntry(key, payload); } LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant) { - if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; - FileHeader header; - std::memcpy(&header, file.data(), sizeof(header)); - if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; - const auto body = file.size() - sizeof(header); - if (header.keyBytes > body || header.payloadBytes != body - header.keyBytes) return LoadStatus::Rejected; - const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); - const auto payload = file.subspan(sizeof(header) + static_cast(header.keyBytes)); - if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; - if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; - if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; Reader reader(payload); CompiledVariant decoded; decodeArtifact(reader, decoded.artifact); From 37ea3b651a249487790e1e231c9ce80fa5e7d568 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 18:26:04 +0300 Subject: [PATCH 133/353] fix(libc): finish the exit handlers when one of them calls exit A guest handler that called exit threw out of the libc exit path because the exiting thread waited on itself. Like FreeBSD, the nested call now runs the handlers that are still registered and ends the process with the status it passed. --- .../prx/libSceSystemService/CMakeLists.txt | 5 ++- core/libs/prx/libc/src/Process.cpp | 9 ++++- core/libs/tests/GuestSystemExit.cpp | 37 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 9b90ef5883..099f5a1906 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -13,14 +13,17 @@ if(BUILD_TESTING) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) + add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") set_tests_properties(guest_system_relaunch PROPERTIES PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) + set_tests_properties(guest_nested_exit PROPERTIES + PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index fd810843e1..76a702a2a6 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -33,6 +33,7 @@ std::stop_source shutdownSource; std::atomic exitRequested{false}; std::atomic exitStarted{false}; using GuestExitCallback = void (APS5_VABI*)(); +thread_local bool runningExit = false; void APS5_VABI RunGuestExitCallback(void* callback) { reinterpret_cast(callback)(); @@ -115,8 +116,12 @@ extern "C" { [[noreturn]] void LibcExit_nid_no_patch(int code) { exitRequested.store(true); - if (exitStarted.exchange(true)) LibcAwaitExit_nid_postfix(); - LibcRunShutdown_nid_postfix(); + if (exitStarted.exchange(true)) { + if (!runningExit) LibcAwaitExit_nid_postfix(); + } else { + runningExit = true; + LibcRunShutdown_nid_postfix(); + } CxaFinalize_nid_no_patch(nullptr); std::exit(code); } diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 56c4146351..2f712816f2 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -20,6 +20,7 @@ extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); extern "C" void APS5_VABI _Exit_nid_postfix(int); +extern "C" void APS5_VABI exit_nid_postfix(int); extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int); using GuestQuickExitCallback = void (APS5_VABI*)(); extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); @@ -71,9 +72,11 @@ void AppendMarker(const std::string& line) { } void MainShutdown() { AppendMarker("shutdown"); } void SecondaryShutdown() { AppendMarker("secondary shutdown"); } -int RunRelaunchingChild(const std::filesystem::path& self) { +void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } +void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } +int RunChild(const std::filesystem::path& self, const char* mode) { #ifdef _WIN32 - std::wstring commandLine = L"\"" + self.wstring() + L"\" --relaunch"; + std::wstring commandLine = L"\"" + self.wstring() + L"\" " + std::wstring(mode, mode + std::strlen(mode)); STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -88,7 +91,7 @@ int RunRelaunchingChild(const std::filesystem::path& self) { const pid_t child = fork(); Require(child >= 0); if (child == 0) { - execl(self.c_str(), self.c_str(), "--relaunch", static_cast(nullptr)); + execl(self.c_str(), self.c_str(), mode, static_cast(nullptr)); std::_Exit(127); } int status = 0; @@ -96,6 +99,23 @@ int RunRelaunchingChild(const std::filesystem::path& self) { return WIFEXITED(status) ? WEXITSTATUS(status) : 1; #endif } +int DriveNestedExit() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + const int code = RunChild(self, "--nested-exit"); + std::string text; + { + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(code == 7); + Require(text == "remaining handler\n"); + std::puts("Guest nested exit completed"); + return 0; +} int DriveRelaunch() { const auto self = OwnExecutable(); const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); @@ -103,7 +123,7 @@ int DriveRelaunch() { const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); SetVariable(RelaunchMarker, marker.string().c_str()); SetVariable(MainExecutable, self.string().c_str()); - Require(RunRelaunchingChild(self) == 0); + Require(RunChild(self, "--relaunch") == 0); std::string text; for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); @@ -140,6 +160,15 @@ int main(int argc, char** argv) { quick_exit_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--nested-exit-driver") == 0) { + return DriveNestedExit(); + } + if (argc > 1 && std::strcmp(argv[1], "--nested-exit") == 0) { + Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { return DriveRelaunch(); } From 0c144502e50e8846a90518b334f1ca2c30339851 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 18:31:17 +0200 Subject: [PATCH 134/353] feat(recompiler): store front-end resource plans in the shader disk cache The disk cache keeps compiled variants (SPIR-V, shader info, binding layout) but not the IrResourcePlan the front end extracts for the resource walk, so a process can only get a plan by running the whole front end. Since #471 the plan owns its values (ownPlanValues clones the graph behind every root into valueStorage and blockStorage), so it can be written out and rebuilt. - EncodePlan writes the value graph in storage order, each value as LEB128 integers: opcode, type, id, flags, register, operand count, the immediate if it has one, each argument as the zigzag distance to the value's own index (0 for null) and, for phis, the index of each predecessor block. The roots (descriptor source dwords, SRT reads, control-flow conditions, uniform-fill values) are value indices; the plain data (memory info, indirect image tables, control-flow edges, materialization sources, guarded, clean and pure flat slots, the flags, the ShaderInfo through the variant's encoder) follows. A plan whose roots or arguments point outside its storage is not encoded. Control-flow conditions can pull most of a program into the plan: a synthetic 2,038-dword compute shader with a branch every 20 dwords has 42,775 plan values, which take 518 KB this way and 2.5 MB with fixed-width fields; its compiled variant entry is 935 KB. - DecodePlan rebuilds the graph the way ownPlanValues does and rejects an unknown opcode, an overlong integer, an index past the values or blocks, a descriptor source, image, buffer or sampler past the sources, a branch without two successors, an edge past the blocks, and a uniform fill of more than four words. - BuildPlanKey is the in-memory source key (RecompileCacheKey::Build and the host subgroup size), every code word and the switch key, after a plan entry tag. It leaves out the binding layout, so all layouts of a shader share one plan. - EncodePlanEntry/DecodePlanEntry use the variant's header and hashes; LoadPlan and StorePlan use the same directory and background writer and count plan hits, misses, writes and load failures on their own. - Size asserts cover the plan and the structures it is written from. - FormatVersion 30. agc_shader_disk_cache now round-trips the plans of 14 in-tree shader fixtures (SRT reads, guarded and chained pointers, control-flow conditions, an indirect image table, a loop phi, a fragment shader) and a hand-built plan with a uniform fill. For each it materializes the original and the decoded plan over 1000 random user-data and memory runtimes and requires the same snapshot, poison, null-root count and read trace, or the same error. Truncated, bit-flipped and other-format entries are rejected, malformed plans do not decode, and the key follows the code, the target and the host subgroup size but not the layout, addresses or user data. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 480 ++++++++++++++++ core/shader/recompiler/ShaderDiskCache.cpp | 511 ++++++++++++++++-- core/shader/recompiler/ShaderDiskCache.hpp | 15 +- 3 files changed, 964 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6ab1458817..2413d62889 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -7,6 +7,9 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "ShaderCacheDirectory.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/RequestMemoryView.hpp" +#include "Optimization/SrtWalker.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include #include #include @@ -18,7 +21,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -1082,6 +1088,478 @@ void verifySpecializationLiveness() { require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable"); } +constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull; +constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull; +constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull; +constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull; +constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull; +constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull; +constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull; +constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull; + +struct PlanFixture { + std::string name; + ShaderStage stage = ShaderStage::Compute; + std::uint64_t codeAddress = 0; + std::uint32_t waveSize = 64; + std::vector code; + std::vector userData; + std::vector>> regions; + std::optional pixel; + std::vector memory; + + RecompileRequest Request(bool useCache) { + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + memory.clear(); + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RecompileRequest request{}; + request.shader = {stage, codeAddress, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.pixel = pixel; + request.context.memory = memory; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = 1u; + request.layout.pushConstantSizeBytes = 128; + request.useCache = useCache; + return request; + } +}; + +std::vector imageWords(std::uint64_t base) { + return {static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; +} + +std::vector bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) { + return {static_cast(base), static_cast((base >> 32u) & 0xffffu) | (stride << 16u), records, format}; +} + +void place(std::vector& words, std::size_t offset, const std::vector& values) { + std::copy(values.begin(), values.end(), words.begin() + static_cast(offset)); +} + +std::vector planFixtures() { + std::vector fixtures; + const std::vector srtPointer{static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u)}; + const auto add = [&](std::string name, std::uint32_t waveSize, std::vector code, std::vector userData, std::vector>> regions) { + PlanFixture fixture; + fixture.name = std::move(name); + fixture.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fixture.waveSize = waveSize; + fixture.code = std::move(code); + fixture.userData = std::move(userData); + fixture.regions = std::move(regions); + fixtures.push_back(std::move(fixture)); + }; + add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {}); + + std::vector guarded(32, 0u); + place(guarded, 0, imageWords(PlanTextureAddress)); + place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + guarded[20] = 1u; + add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + + std::vector heap(32, 0u); + place(heap, 0, imageWords(PlanTextureAddress)); + place(heap, 8, imageWords(PlanTextureAddress + 0x100u)); + place(heap, 24, imageWords(PlanTextureAddress)); + const std::vector materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u}; + std::vector tables(16, 0u); + place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u)); + place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u)); + place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u)); + add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + + std::vector phis(32, 0u); + place(phis, 0, imageWords(PlanTextureAddress)); + place(phis, 12, {0x49u, 0u, 0x00500000u, 0u}); + place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + phis[20] = 1u; + place(phis, 24, imageWords(PlanTextureAddress + 0x100u)); + add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + + std::vector root(16, 0u); + place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)); + root[4] = 1u; + root[6] = static_cast(PlanPayloadAddress); + root[7] = static_cast(PlanPayloadAddress >> 32u); + const std::vector payload{0x3f800000u}; + add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}}); + auto chained = root; + chained[6] = static_cast(PlanPointerAddress); + chained[7] = static_cast(PlanPointerAddress >> 32u); + add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast(PlanPayloadAddress), static_cast(PlanPayloadAddress >> 32u)}}}); + add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}}); + + PlanFixture fragment; + fragment.name = "fragment parameters"; + fragment.stage = ShaderStage::Fragment; + fragment.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u}; + ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 2u; + pixel.interpolatorSettings[1] = 1u; + pixel.wave32 = true; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.noPerspective = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping[0] = 0xe4u; + fragment.pixel = pixel; + fixtures.push_back(std::move(fragment)); + return fixtures; +} + +IrResourcePlan handBuiltPlan() { + IrResourcePlan plan; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + plan.valueStorage.push_back(std::make_unique(opcode, type, ids++)); + return *plan.valueStorage.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource); + const auto readConst = [&](std::uint32_t slot) -> IrValue& { + auto& read = make(IrOpcode::ReadConst, IrType::U32); + read.AddArgument(&resource); + read.AddArgument(&constant(slot)); + return read; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64); + handle.AddArgument(&userData(0)); + handle.AddArgument(&userData(1)); + auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32); + pointer.AddArgument(&handle); + pointer.AddArgument(&constant(8)); + plan.blockStorage.push_back(std::make_unique(3u)); + plan.blockStorage.push_back(std::make_unique(7u)); + auto& phi = make(IrOpcode::Phi, IrType::U32); + phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0)); + phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0)); + auto& condition = make(IrOpcode::Void, IrType::Bool); + condition.SetImmediateU64(1u); + condition.SetFlags(std::uint64_t{0x1234u}); + plan.stage = IrShaderStage::Compute; + plan.shaderHash = 0x123456789abcull; + plan.userDataBase = 2u; + plan.userDataCount = 4u; + plan.srgbDecodeFormats = 5u; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.resource = 1u; + memory.dmask = 0xfu; + memory.imageDimension = RdnaImageDimension::Dim2D; + memory.gpuDescriptor = true; + memory.flushDenormals = true; + plan.memoryInfo = {memory, MemoryInfo{}}; + DescriptorSource buffer; + buffer.dwordCount = 4u; + buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer}; + DescriptorSource table; + table.dwordCount = 8u; + table.dwords[0] = &readConst(3); + table.indirectImage = DescriptorSource::IndirectImage{1u, 0u, 16u, 4u, 2u, 32u, true}; + plan.descriptorSources = {buffer, table}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {}, {0u}}, {nullptr, {}, {}, {}}}; + plan.materializationSources = {0u}; + plan.srtReads = {{&readConst(4), 0u}, {&pointer, 1u}}; + plan.guardedSrtSlots = {1u}; + plan.srtGuardOffset = 6u; + plan.cleanFlatSlots = {0u, 1u}; + plan.pureFlatSlots = {1u, 0u}; + plan.requiresSpecializationMemory = true; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.info.buffers = {BufferResource{}}; + ImageResource image{}; + image.source = 1u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + plan.info.images = {image}; + ResourceMaterializer::PrepareImageModes(plan.info); + plan.uniformFill.fill = {UniformFillKind::Buffer, 0u, {4u, 0u, 0u}, 2u, 0u}; + auto& fillValue = readConst(5); + plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr}; + return plan; +} + +std::vector encodedPlan(const IrResourcePlan& plan) { + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode"); + return bytes; +} + +struct MaterializedPlan { + bool ok = false; + std::string error; + ResourceSnapshot snapshot; + SrtReadTrace trace; + std::size_t leaf = 0; +}; + +MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) { + MaterializedPlan outcome; + runtime.readTrace = &outcome.trace; + try { + ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot); + outcome.ok = true; + } catch (const std::exception& error) { + outcome.error = error.what(); + } + outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin()); + return outcome; +} + +bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) { + const auto& a = left.snapshot; + const auto& b = right.snapshot; + return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads && + left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads; +} + +void mutateWords(std::vector& words, std::mt19937& random) { + static constexpr std::array addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress}; + for (auto& word : words) { + switch (random() % 16u) { + case 0: word = static_cast(random()); break; + case 1: word = 0u; break; + case 2: { + const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u; + word = random() % 2u == 0u ? static_cast(address) : static_cast(address >> 32u); + break; + } + case 3: word ^= 1u << (random() % 32u); break; + default: break; + } + } +} + +std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) { + std::mt19937 random(seed); + std::size_t succeeded = 0; + for (std::uint32_t run = 0; run < 1000u; ++run) { + auto userData = fixture.userData; + auto regions = fixture.regions; + if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u); + if (run != 0u) { + mutateWords(userData, random); + for (auto& region : regions) mutateWords(region.second, random); + } + std::vector memory; + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RequestMemoryView view(memory); + const auto runtime = view.MakeRuntime(userData, fixture.codeAddress); + const auto original = materializePlan(plan, runtime); + const auto loaded = materializePlan(decoded, runtime); + require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")"); + if (original.ok) ++succeeded; + } + return succeeded; +} + +void verifyPlanEntryDamage(const std::vector& key, const IrResourcePlan& plan, const std::string& what) { + const auto file = ShaderDiskCache::EncodePlanEntry(key, plan); + require(!file.empty(), what + ": the plan entry was not encoded"); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan"); + for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) { + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected"); + } + for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) { + for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) { + auto damaged = file; + damaged[offset] ^= bit; + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected"); + } + } + auto longer = file; + longer.push_back(std::byte{0}); + require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected"); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads"); + auto previousFormat = file; + previousFormat[4] = static_cast(ShaderDiskCache::FormatVersion - 1u); + require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads"); + auto otherSource = file; + otherSource[8] ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads"); + CompiledVariant variant; + require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant"); +} + +void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) { + const auto bytes = encodedPlan(plan); + for (std::size_t size = 0; size < bytes.size(); ++size) { + IrResourcePlan partial; + require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes"); + } + std::size_t rejected = 0; + for (std::size_t offset = 0; offset < bytes.size(); ++offset) { + for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) { + auto damaged = bytes; + damaged[offset] ^= bit; + IrResourcePlan decoded; + if (!ShaderDiskCache::DecodePlan(damaged, decoded)) { + ++rejected; + continue; + } + std::vector again; + require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained"); + } + } + require(rejected != 0u, what + ": no damaged plan was rejected"); +} + +void verifyMalformedPlans() { + const auto rejects = [](const std::string& what, const std::function& damage) { + auto plan = handBuiltPlan(); + damage(plan); + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode"); + IrResourcePlan decoded; + require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes"); + }; + rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); }); + rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; }); + rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 2u; }); + rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; }); + rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(2u); }); + rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 2u; }); + rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); + rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); + rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); + auto foreign = handBuiltPlan(); + IrValue stray(IrOpcode::Void, IrType::U32, 99u); + foreign.srtReads[0].value = &stray; + std::vector bytes; + require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded"); + auto unblocked = handBuiltPlan(); + auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique(IrOpcode::Phi, IrType::U32, 100u)); + phi.AddArgument(unblocked.valueStorage[0].get()); + unblocked.srtReads[0].value = φ + bytes.clear(); + require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded"); + auto emptied = handBuiltPlan(); + emptied.valueStorage.push_back(nullptr); + bytes.clear(); + require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded"); +} + +void verifyPlanRoundTrip() { + const auto handBuilt = handBuiltPlan(); + const auto handBytes = encodedPlan(handBuilt); + IrResourcePlan handDecoded; + require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode"); + require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); + const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; + require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); + require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); + require(handDecoded.descriptorSources[1].indirectImage == handBuilt.descriptorSources[1].indirectImage && handDecoded.memoryInfo == handBuilt.memoryInfo && handDecoded.info == handBuilt.info, "the decoded plan lost plain data"); + PlanFixture handFixture; + handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; + std::vector handSrt(16, 0u); + place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u)); + handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}}; + static_cast(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan")); + verifyPlanPayloadDamage(handBuilt, "hand-built plan"); + verifyMalformedPlans(); + + auto fixtures = planFixtures(); + std::vector> plans; + std::size_t index = 0; + for (auto& fixture : fixtures) { + auto request = fixture.Request(false); + const auto plan = GetResourcePlan(request); + plans.push_back(plan); + const auto bytes = encodedPlan(*plan); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); + require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); + require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); + const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); + require(succeeded != 0u, fixture.name + ": no runtime materialized"); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, 32u, key); + if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name); + std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; + ++index; + } + const auto covers = [&](const std::function& predicate) { + return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr& plan) { return predicate(*plan); }); + }; + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi"); + require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot"); +} + +void verifyPlanKey() { + auto fixtures = planFixtures(); + auto& fixture = fixtures[1]; + const auto planKey = [&](const std::function& change, std::uint32_t hostSubgroupSize = 32u) { + auto request = fixture.Request(true); + change(request); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key); + return key; + }; + const auto key = planKey([](RecompileRequest&) {}); + require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic"); + std::vector variantKey; + ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey); + require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry"); + require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key"); + require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key"); + auto userData = fixture.userData; + userData[0] ^= 0x100u; + require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key"); + const auto original = fixture.code; + for (std::size_t word = 0; word < original.size(); ++word) { + for (std::uint32_t bit = 0; bit < 32u; ++bit) { + fixture.code = original; + fixture.code[word] ^= 1u << bit; + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit)); + } + } + fixture.code = original; + fixture.code.push_back(0xbf810000u); + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word"); + fixture.code = original; + require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size"); + require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target"); + require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader"); + require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size"); + require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -1110,6 +1588,8 @@ int main(int argc, char** argv) { verifyArtifactStorageIsolation(); verifyKeySensitivity(); verifyFloatModeSerialization(); + verifyPlanRoundTrip(); + verifyPlanKey(); verifyStore(); verifyAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 713bff2c8b..841909dc09 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include namespace ShaderRecompiler { @@ -94,6 +96,16 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 152, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 80, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); +static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); +static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 104, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource::IndirectImage) == 28, "IndirectImage changed: update the plan encoder"); +static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); +static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); +static_assert(sizeof(UniformFill) == 28, "UniformFill changed: update the plan encoder"); +static_assert(sizeof(UniformFillPlan) == 64, "UniformFillPlan changed: update the plan encoder"); #endif namespace { @@ -138,6 +150,14 @@ public: for (const bool flag : flags) Value(flag); } + void Compact(std::uint64_t value) { + while (value >= 0x80u) { + Raw(static_cast(value | 0x80u)); + value >>= 7u; + } + Raw(static_cast(value)); + } + void Text(const std::string& text) { Value(text.size()); const auto bytes = std::as_bytes(std::span(text.data(), text.size())); @@ -167,6 +187,22 @@ public: [[nodiscard]] bool Ok() const { return ok; } [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + [[nodiscard]] std::size_t Remaining() const { return ok ? bytes.size() - position : 0; } + void Fail() { ok = false; } + + [[nodiscard]] std::uint64_t Compact() { + std::uint64_t value = 0; + for (std::uint32_t shift = 0; shift < 64u; shift += 7u) { + const auto byte = Raw(); + if (!ok) return 0; + value |= static_cast(byte & 0x7fu) << shift; + if ((byte & 0x80u) != 0u) continue; + if ((byte == 0u && shift != 0u) || (shift == 63u && byte > 1u)) ok = false; + return ok ? value : 0; + } + ok = false; + return 0; + } template void Value(TValue& value) requires (std::is_integral_v || std::is_enum_v) { @@ -690,6 +726,321 @@ void decodeAllocation(Reader& reader, CompiledBindingLayout& allocation) { reader.Value(allocation.pushConstantSizeBytes); } +constexpr std::uint32_t PlanEntryKind = 0x4e414c50u; +constexpr std::uint32_t NoPlanIndex = std::numeric_limits::max(); + +void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { + out.Value(memory.kind); + out.Value(memory.resource); + out.Value(memory.sampler); + out.Value(memory.offset); + out.Value(memory.secondaryOffset); + out.Value(memory.dmask); + out.Value(memory.dataDwords); + out.Value(memory.dataBits); + out.Value(memory.componentIndex); + out.Value(memory.componentCount); + out.Value(memory.dataFormat); + out.Value(memory.numberFormat); + out.Value(memory.imageSampleFlags); + out.Value(memory.imageDimension); + out.Value(memory.imageAddressComponents); + out.Value(memory.addressIsFull); + out.Value(memory.dataSigned); + out.Value(memory.typed); + out.Value(memory.formatted); + out.Value(memory.d16); + out.Value(memory.imageHasMip); + out.Value(memory.imageR128); + out.Value(memory.imagePacked); + out.Value(memory.imageByElements); + out.Value(memory.idxen); + out.Value(memory.offen); + out.Value(memory.planningOnly); + out.Value(memory.coherent); + out.Value(memory.gpuDescriptor); + out.Value(memory.flushDenormals); +} + +void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { + in.Value(memory.kind); + in.Value(memory.resource); + in.Value(memory.sampler); + in.Value(memory.offset); + in.Value(memory.secondaryOffset); + in.Value(memory.dmask); + in.Value(memory.dataDwords); + in.Value(memory.dataBits); + in.Value(memory.componentIndex); + in.Value(memory.componentCount); + in.Value(memory.dataFormat); + in.Value(memory.numberFormat); + in.Value(memory.imageSampleFlags); + in.Value(memory.imageDimension); + in.Value(memory.imageAddressComponents); + in.Value(memory.addressIsFull); + in.Value(memory.dataSigned); + in.Value(memory.typed); + in.Value(memory.formatted); + in.Value(memory.d16); + in.Value(memory.imageHasMip); + in.Value(memory.imageR128); + in.Value(memory.imagePacked); + in.Value(memory.imageByElements); + in.Value(memory.idxen); + in.Value(memory.offen); + in.Value(memory.planningOnly); + in.Value(memory.coherent); + in.Value(memory.gpuDescriptor); + in.Value(memory.flushDenormals); +} + +bool encodePlan(Writer& writer, const IrResourcePlan& plan) { + if (std::ranges::any_of(plan.valueStorage, [](const auto& value) { return value == nullptr; }) || std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; + std::unordered_map values; + std::unordered_map blocks; + values.reserve(plan.valueStorage.size()); + blocks.reserve(plan.blockStorage.size()); + for (std::size_t index = 0; index < plan.valueStorage.size(); ++index) values.emplace(plan.valueStorage[index].get(), static_cast(index)); + for (std::size_t index = 0; index < plan.blockStorage.size(); ++index) blocks.emplace(plan.blockStorage[index].get(), static_cast(index)); + bool owned = values.size() == plan.valueStorage.size() && blocks.size() == plan.blockStorage.size(); + std::uint32_t current = 0; + const auto valueIndex = [&](const IrValue* value) { + if (value == nullptr) return NoPlanIndex; + const auto found = values.find(value); + if (found == values.end()) { + owned = false; + return NoPlanIndex; + } + return found->second; + }; + writer.Value(plan.stage); + writer.Value(plan.shaderHash); + writer.Value(plan.userDataBase); + writer.Value(plan.userDataCount); + writer.Value(plan.srgbDecodeFormats); + writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); + writer.List(plan.valueStorage, [&](Writer& out, const std::unique_ptr& value) { + const auto bank = static_cast(value->Register().bank); + if (bank > 0xfu) owned = false; + out.Compact(static_cast(value->Opcode())); + out.Compact(static_cast(value->Type())); + out.Compact(value->Id()); + out.Compact(value->Flags()); + out.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); + out.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); + if (value->HasImmediate()) out.Compact(value->ImmediateU64()); + for (const auto* argument : value->Arguments()) { + const auto index = valueIndex(argument); + const auto delta = static_cast(index) - static_cast(current); + out.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); + } + if (value->IsPhi() && value->PhiBlockCount() != value->ArgumentCount()) owned = false; + for (std::size_t operand = 0; value->IsPhi() && operand < value->ArgumentCount(); ++operand) { + const auto found = operand < value->PhiBlockCount() ? blocks.find(value->PhiBlock(operand)) : blocks.end(); + if (found == blocks.end()) owned = false; + out.Compact(found != blocks.end() ? found->second : 0u); + } + ++current; + }); + writer.List(plan.memoryInfo, encodeMemoryInfo); + writer.List(plan.descriptorSources, [&](Writer& out, const DescriptorSource& source) { + for (const auto* dword : source.dwords) out.Value(valueIndex(dword)); + out.Value(source.dwordCount); + const auto table = source.indirectImage.value_or(DescriptorSource::IndirectImage{}); + out.Value(source.indirectImage.has_value()); + out.Value(table.materialSource); + out.Value(table.heapSource); + out.Value(table.selectorStride); + out.Value(table.selectorOffset); + out.Value(table.keyArg); + out.Value(table.entryOffset); + out.Value(table.hasMaterial); + }); + writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { + out.Value(valueIndex(block.condition)); + out.Values(std::span(block.successors)); + out.Values(std::span(block.sources)); + out.Values(std::span(block.images)); + }); + writer.Values(std::span(plan.materializationSources)); + writer.List(plan.srtReads, [&](Writer& out, const SrtRead& read) { + out.Value(valueIndex(read.value)); + out.Value(read.flatOffset); + }); + writer.Values(std::span(plan.guardedSrtSlots)); + writer.Value(plan.srtGuardOffset); + writer.Values(std::span(plan.cleanFlatSlots)); + writer.Values(std::span(plan.pureFlatSlots)); + writer.Value(plan.requiresSpecializationMemory); + writer.Value(plan.srtPlanComplete); + writer.Value(plan.resourceTrackingComplete); + encodeShaderInfo(writer, plan.info); + const auto& fill = plan.uniformFill.fill; + writer.Value(fill.kind); + writer.Value(fill.resource); + for (const auto stride : fill.groupStride) writer.Value(stride); + writer.Value(fill.words); + writer.Value(fill.value); + for (const auto* value : plan.uniformFill.values) writer.Value(valueIndex(value)); + return owned; +} + +struct PlanValueRecord { + IrOpcode opcode = IrOpcode::Identity; + IrType type = IrType::Void; + std::uint32_t id = 0; + std::uint64_t flags = 0; + bool hasImmediate = false; + std::uint64_t immediate = 0; + GuestRegister registerValue{}; + std::vector arguments; + std::vector phiBlocks; +}; + +bool decodePlan(Reader& reader, IrResourcePlan& plan) { + reader.Value(plan.stage); + reader.Value(plan.shaderHash); + reader.Value(plan.userDataBase); + reader.Value(plan.userDataCount); + reader.Value(plan.srgbDecodeFormats); + std::vector blockIds; + reader.Values(blockIds); + std::vector records; + std::uint32_t current = 0; + reader.List(records, 6, [&](Reader& in, PlanValueRecord& record) { + constexpr std::uint64_t Word = std::numeric_limits::max(); + const auto opcode = in.Compact(); + const auto type = in.Compact(); + const auto id = in.Compact(); + record.flags = in.Compact(); + const auto registerValue = in.Compact(); + const auto shape = in.Compact(); + const auto arguments = shape >> 1u; + if (opcode >= static_cast(IrOpcode::Count) || type > Word || id > Word || (registerValue >> 4u) > Word || arguments > in.Remaining()) { + in.Fail(); + return; + } + record.opcode = static_cast(opcode); + record.type = static_cast(type); + record.id = static_cast(id); + record.registerValue = {static_cast(registerValue & 0xfu), static_cast(registerValue >> 4u)}; + record.hasImmediate = (shape & 1u) != 0u; + if (record.hasImmediate) record.immediate = in.Compact(); + record.arguments.resize(static_cast(arguments)); + for (auto& argument : record.arguments) { + const auto code = in.Compact(); + if (code == 0u) { + argument = NoPlanIndex; + continue; + } + const auto zigzag = code - 1u; + const auto delta = static_cast(zigzag >> 1u) ^ -static_cast(zigzag & 1u); + const auto index = static_cast(current) + delta; + if (index < 0 || index >= static_cast(records.size())) { + in.Fail(); + return; + } + argument = static_cast(index); + } + if (record.opcode == IrOpcode::Phi) { + record.phiBlocks.resize(record.arguments.size()); + for (auto& block : record.phiBlocks) { + const auto index = in.Compact(); + block = index < blockIds.size() ? static_cast(index) : NoPlanIndex; + if (block == NoPlanIndex) in.Fail(); + } + } + ++current; + }); + if (!reader.Ok()) return false; + plan.blockStorage.reserve(blockIds.size()); + for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); + plan.valueStorage.reserve(records.size()); + for (const auto& record : records) { + auto value = std::make_unique(record.opcode, record.type, record.id); + value->SetFlags(record.flags); + if (record.hasImmediate) value->SetImmediateU64(record.immediate); + value->SetRegister(record.registerValue); + plan.valueStorage.push_back(std::move(value)); + } + bool valid = true; + const auto valueAt = [&](std::uint32_t index) -> IrValue* { + if (index == NoPlanIndex) return nullptr; + if (index >= plan.valueStorage.size()) { + valid = false; + return nullptr; + } + return plan.valueStorage[index].get(); + }; + for (std::size_t index = 0; index < records.size(); ++index) { + const auto& record = records[index]; + auto& value = *plan.valueStorage[index]; + for (std::size_t operand = 0; operand < record.arguments.size(); ++operand) { + auto* argument = valueAt(record.arguments[operand]); + if (value.IsPhi()) { + value.AddPhiOperand(plan.blockStorage[record.phiBlocks[operand]].get(), argument); + } else { + value.AddArgument(argument); + } + } + } + reader.List(plan.memoryInfo, 78, decodeMemoryInfo); + reader.List(plan.descriptorSources, 62, [&](Reader& in, DescriptorSource& source) { + for (auto& dword : source.dwords) dword = valueAt(in.Get()); + in.Value(source.dwordCount); + const bool indirect = in.Get(); + DescriptorSource::IndirectImage table; + in.Value(table.materialSource); + in.Value(table.heapSource); + in.Value(table.selectorStride); + in.Value(table.selectorOffset); + in.Value(table.keyArg); + in.Value(table.entryOffset); + in.Value(table.hasMaterial); + source.indirectImage = indirect ? std::optional(table) : std::nullopt; + }); + reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { + block.condition = valueAt(in.Get()); + in.Values(block.successors); + in.Values(block.sources); + in.Values(block.images); + }); + reader.Values(plan.materializationSources); + reader.List(plan.srtReads, 8, [&](Reader& in, SrtRead& read) { + read.value = valueAt(in.Get()); + in.Value(read.flatOffset); + }); + reader.Values(plan.guardedSrtSlots); + reader.Value(plan.srtGuardOffset); + reader.Values(plan.cleanFlatSlots); + reader.Values(plan.pureFlatSlots); + reader.Value(plan.requiresSpecializationMemory); + reader.Value(plan.srtPlanComplete); + reader.Value(plan.resourceTrackingComplete); + decodeShaderInfo(reader, plan.info); + auto& fill = plan.uniformFill.fill; + reader.Value(fill.kind); + reader.Value(fill.resource); + for (auto& stride : fill.groupStride) reader.Value(stride); + reader.Value(fill.words); + reader.Value(fill.value); + for (auto& value : plan.uniformFill.values) value = valueAt(reader.Get()); + if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; + const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; + if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; + if (!std::ranges::all_of(plan.descriptorSources, [](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size(); })) return false; + if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; + if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; + if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; + return std::ranges::all_of(plan.controlFlow, [&](const ResourceBlock& block) { + return (block.condition == nullptr || block.successors.size() == 2u) && + std::ranges::all_of(block.successors, [&](std::uint32_t successor) { return successor < plan.controlFlow.size(); }) && + std::ranges::all_of(block.sources, knownSource) && + std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }); + }); +} + constexpr std::string_view NeutralSwitches[] = { "APS5_PROFILE_DRAW", "APS5_DUMP_IR", @@ -761,43 +1112,19 @@ public: [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { - const auto started = std::chrono::steady_clock::now(); - const auto name = EntryName(key); - thread_local std::vector* fileSlot = nullptr; - auto& file = ThreadOwned(fileSlot); - bool loaded = false; - if (!ReadWholeFile(directory / name, file)) { - misses.fetch_add(1, std::memory_order_relaxed); - } else { - switch (DecodeEntry(file, key, variant)) { - case LoadStatus::Loaded: - hits.fetch_add(1, std::memory_order_relaxed); - bytesRead.fetch_add(file.size(), std::memory_order_relaxed); - loaded = true; - break; - case LoadStatus::Absent: - case LoadStatus::KeyMismatch: - misses.fetch_add(1, std::memory_order_relaxed); - break; - case LoadStatus::Rejected: { - const auto failures = loadFailures.fetch_add(1, std::memory_order_relaxed); - if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); - break; - } - } - } - if (file.capacity() > (4u << 20u)) std::vector().swap(file); - loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - report(false); - return loaded; + return load(key, [&](std::span file) { return DecodeEntry(file, key, variant); }, hits, misses, loadFailures); + } + + bool LoadPlan(std::span key, IrResourcePlan& plan) { + return load(key, [&](std::span file) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); } void Store(std::vector key, std::shared_ptr variant) { - { - std::lock_guard lock(mutex); - jobs.push_back({std::move(key), std::move(variant)}); - } - wake.notify_one(); + enqueue({std::move(key), std::move(variant), nullptr}); + } + + void StorePlan(std::vector key, std::shared_ptr plan) { + enqueue({std::move(key), nullptr, std::move(plan)}); } void Flush() { @@ -814,6 +1141,10 @@ public: counters.writeFailures = writeFailures.load(std::memory_order_relaxed); counters.bytesRead = bytesRead.load(std::memory_order_relaxed); counters.bytesWritten = bytesWritten.load(std::memory_order_relaxed); + counters.planHits = planHits.load(std::memory_order_relaxed); + counters.planMisses = planMisses.load(std::memory_order_relaxed); + counters.planWrites = planWrites.load(std::memory_order_relaxed); + counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); return counters; } @@ -821,8 +1152,50 @@ private: struct Job { std::vector key; std::shared_ptr variant; + std::shared_ptr plan; }; + template + bool load(std::span key, TDecode&& decode, std::atomic& hitCount, std::atomic& missCount, std::atomic& failureCount) { + const auto started = std::chrono::steady_clock::now(); + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + bool loaded = false; + if (!ReadWholeFile(directory / name, file)) { + missCount.fetch_add(1, std::memory_order_relaxed); + } else { + switch (decode(std::span(file))) { + case LoadStatus::Loaded: + hitCount.fetch_add(1, std::memory_order_relaxed); + bytesRead.fetch_add(file.size(), std::memory_order_relaxed); + loaded = true; + break; + case LoadStatus::Absent: + case LoadStatus::KeyMismatch: + missCount.fetch_add(1, std::memory_order_relaxed); + break; + case LoadStatus::Rejected: { + const auto failures = failureCount.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); + break; + } + } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + report(false); + return loaded; + } + + void enqueue(Job job) { + { + std::lock_guard lock(mutex); + jobs.push_back(std::move(job)); + } + wake.notify_one(); + } + void run() { std::fprintf(stderr, "[shader-disk-cache] %s (source version %s, format %u)\n", directory.string().c_str(), hex(SourceVersion()).c_str(), FormatVersion); housekeeping(); @@ -847,9 +1220,9 @@ private: } void write(const Job& job) { - const auto file = EncodeEntry(job.key, *job.variant); - if (WriteFileAtomically(directory / EntryName(job.key), file)) { - writes.fetch_add(1, std::memory_order_relaxed); + const auto file = job.plan != nullptr ? EncodePlanEntry(job.key, *job.plan) : EncodeEntry(job.key, *job.variant); + if (!file.empty() && WriteFileAtomically(directory / EntryName(job.key), file)) { + (job.plan != nullptr ? planWrites : writes).fetch_add(1, std::memory_order_relaxed); bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); } else { const auto failures = writeFailures.fetch_add(1, std::memory_order_relaxed); @@ -899,11 +1272,13 @@ private: const auto totals = Totals(); const auto loadNs = loadNanoseconds.load(std::memory_order_relaxed); const auto delta = [](std::uint64_t after, std::uint64_t before) { return static_cast(after - before); }; - if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes)\n", + if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures && totals.planHits == reported.planHits && totals.planMisses == reported.planMisses && totals.planWrites == reported.planWrites && totals.planLoadFailures == reported.planLoadFailures) return; + std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", delta(totals.hits, reported.hits), delta(totals.misses, reported.misses), delta(totals.writes, reported.writes), static_cast(totals.bytesWritten - reported.bytesWritten) / (1024.0 * 1024.0), - delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, - static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes)); + delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), + delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), + static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, + static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); reported = totals; reportedLoadNs = loadNs; } @@ -916,6 +1291,7 @@ private: std::deque jobs; bool writing = false; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; + std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; std::atomic lastReport{0}; std::mutex reportMutex; Counters reported; @@ -953,6 +1329,23 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s key.insert(key.end(), switches.begin(), switches.end()); } +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key) { + key.clear(); + Writer writer(key); + writer.Value(FileMagic); + writer.Value(FormatVersion); + writer.Value(SourceVersion()); + writer.Value(PlanEntryKind); + thread_local std::vector* memoryKeySlot = nullptr; + auto& memoryKey = ThreadOwned(memoryKeySlot); + RecompileCacheKey::Build(request, memoryKey); + writer.Values(std::span(memoryKey)); + writer.Values(request.shader.code); + writer.Value(hostSubgroupSize); + const auto& switches = switchKey(); + key.insert(key.end(), switches.begin(), switches.end()); +} + std::string EntryName(std::span key) { return hex(HashBytes(key, 0x5eed0001ull)) + hex(HashBytes(key, 0x5eed0002ull)) + ".bin"; } @@ -999,6 +1392,32 @@ LoadStatus DecodeEntry(std::span file, std::span& out) { + Writer writer(out); + return encodePlan(writer, plan); +} + +bool DecodePlan(std::span bytes, IrResourcePlan& plan) { + Reader reader(bytes); + IrResourcePlan decoded; + if (!decodePlan(reader, decoded)) return false; + plan = std::move(decoded); + return true; +} + +std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan) { + std::vector payload; + if (!EncodePlan(plan, payload)) return {}; + return wrapEntry(key, payload); +} + +LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan) { + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; + return DecodePlan(payload, plan) ? LoadStatus::Loaded : LoadStatus::Rejected; +} + bool Enabled() { return store().Enabled(); } @@ -1017,6 +1436,16 @@ void Store(std::vector key, std::shared_ptr va if (instance.Enabled()) instance.Store(std::move(key), std::move(variant)); } +bool LoadPlan(std::span key, IrResourcePlan& plan) { + auto& instance = store(); + return instance.Enabled() && instance.LoadPlan(key, plan); +} + +void StorePlan(std::vector key, std::shared_ptr plan) { + auto& instance = store(); + if (instance.Enabled() && plan != nullptr) instance.StorePlan(std::move(key), std::move(plan)); +} + void Flush() { auto& instance = store(); if (instance.Enabled()) instance.Flush(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 81bbd87beb..530ccae9c4 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILER_SHADERDISKCACHE_HPP #include "CompiledVariant.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include #include #include @@ -12,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 29; +inline constexpr std::uint32_t FormatVersion = 30; enum class LoadStatus { Loaded, @@ -29,6 +30,10 @@ struct Counters { std::uint64_t writeFailures = 0; std::uint64_t bytesRead = 0; std::uint64_t bytesWritten = 0; + std::uint64_t planHits = 0; + std::uint64_t planMisses = 0; + std::uint64_t planWrites = 0; + std::uint64_t planLoadFailures = 0; }; [[nodiscard]] std::uint64_t SourceVersion(); @@ -43,11 +48,19 @@ void EncodeResult(const RecompileResult& result, std::vector& out); [[nodiscard]] std::vector EncodeEntry(std::span key, const CompiledVariant& variant); [[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant); +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key); +[[nodiscard]] bool EncodePlan(const IrResourcePlan& plan, std::vector& out); +[[nodiscard]] bool DecodePlan(std::span bytes, IrResourcePlan& plan); +[[nodiscard]] std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan); +[[nodiscard]] LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan); + [[nodiscard]] bool Enabled(); [[nodiscard]] std::filesystem::path EntryDirectory(); [[nodiscard]] bool Load(std::span key, CompiledVariant& variant); void Store(std::vector key, std::shared_ptr variant); +[[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); +void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); [[nodiscard]] Counters Totals(); From 1ed6c2106715338b3b37f4811c90524e6ba96dcd Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:23:33 +0200 Subject: [PATCH 135/353] perf(recompiler): load the resource plan from the disk cache before the front end getSource built ResourceProgram (PrepareResourceProgram and ExtractPlan) for the first request of every shader a process saw, before findOrCompileVariant looked in the disk cache. With a warm cache every launch still decoded, built the CFG, structurized, translated and ran SSA, the fold passes, SrtWalker and ResourceTracker for every shader; only SPIR-V emission was skipped. getSource now builds the plan key and tries LoadPlan first. On a hit it sets the plan and leaves the program null: findOrCompileVariant already rebuilds the program when no stored variant fits the layout. The loaded plan takes the request's code address as its shader hash, as a plan built in this process would. On a miss it builds the plan as before and stores it. A plan the front end refuses is not stored. The disk is not used when the cache is off, under the debug probe, with APS5_DUMP_IR (which prints the front-end passes), or with APS5_NO_PERF_DISK_PLANS=1, a cache-neutral switch for comparisons. FrontEndBuilds() counts PrepareResourceProgram calls. The new cross-process test prepares, captures and materializes 14 shader fixtures in one process and repeats it in a second process sharing the cache directory: the second process must run the front end 0 times, hit 14 plans and 14 variants, and produce byte-identical SPIR-V, bindings, push constants, snapshots, read traces and plans. A damaged plan entry is rejected and rebuilt alone with the same output and stored again; a changed code word and another switch value miss. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 146 ++++++++++++++++++ .../include/Optimization/ResourceProgram.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 42 ++++- core/shader/recompiler/ShaderDiskCache.cpp | 1 + 4 files changed, 183 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 2413d62889..a732408bce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1560,6 +1560,150 @@ void verifyPlanKey() { require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode"); } +void appendWords(std::vector& out, std::span words) { + const auto count = static_cast(words.size()); + const auto countBytes = std::as_bytes(std::span(&count, 1)); + out.insert(out.end(), countBytes.begin(), countBytes.end()); + const auto bytes = std::as_bytes(words); + out.insert(out.end(), bytes.begin(), bytes.end()); +} + +void appendSnapshot(std::vector& out, const ResourceCapture& capture) { + const auto& snapshot = capture.snapshot; + for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) { + std::vector words; + for (const auto& value : *values) { + words.push_back(value.dwordCount); + words.insert(words.end(), value.dwords.begin(), value.dwords.end()); + } + appendWords(out, words); + } + appendWords(out, snapshot.flattenedSrt); + appendWords(out, snapshot.userData); + const auto& fill = snapshot.uniformFill; + appendWords(out, std::vector{static_cast(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads}); + std::vector poison; + for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast(entry.address), static_cast(entry.address >> 32u)}); + appendWords(out, poison); + std::vector leaves{capture.readTrace.leafSlot}; + for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast(address), static_cast(address >> 32u)}); + appendWords(out, leaves); + std::vector reads; + for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast(address), static_cast(address >> 32u)}); + appendWords(out, reads); +} + +std::vector> preparedOutputs(std::vector& fixtures) { + std::vector> outputs; + for (auto& fixture : fixtures) { + const auto request = fixture.Request(true); + const auto handle = PrepareShader(request); + RequestMemoryView view(request.context.memory); + const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle); + const auto result = MaterializeShader(request, *capture, *handle); + std::vector bytes; + appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words()); + ShaderDiskCache::EncodeResult(*result, bytes); + appendSnapshot(bytes, *capture); + require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode"); + outputs.push_back(std::move(bytes)); + } + return outputs; +} + +std::filesystem::path planOutputPath(std::size_t index) { + return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin"); +} + +std::filesystem::path planEntryPath(PlanFixture& fixture) { + std::vector key; + ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key); + return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +} + +int runPlanLoadingProcess(std::string_view mode) { + if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); + auto fixtures = planFixtures(); + const auto count = fixtures.size(); + const auto outputs = preparedOutputs(fixtures); + ShaderDiskCache::Flush(); + const auto totals = ShaderDiskCache::Totals(); + const auto builds = FrontEndBuilds(); + const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")"; + if (mode == "--load-plan-switch") { + require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts); + std::cout << "a process with another switch value rebuilt all " << count << " plans\n"; + return 0; + } + if (mode == "--load-plan") { + require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts); + } else { + require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts); + } + for (std::size_t index = 0; index < count; ++index) { + std::vector expected; + require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name); + require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process"); + } + if (mode == "--load-plan") { + auto changed = planFixtures(); + auto& fixture = changed.front(); + fixture.code.insert(fixture.code.end() - 1, 0xbf800000u); + const auto request = fixture.Request(true); + static_cast(PrepareShader(request)); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan"); + std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n"; + } else { + std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; + } + ShaderDiskCache::Flush(); + return 0; +} + +void verifyPlanAcrossProcesses(const char* self) { + auto fixtures = planFixtures(); + const auto builds = FrontEndBuilds(); + const auto before = ShaderDiskCache::Totals(); + const auto outputs = preparedOutputs(fixtures); + ShaderDiskCache::Flush(); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan"); + std::filesystem::create_directories(planOutputPath(0).parent_path()); + for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output"); + auto refused = fixtures[1]; + refused.codeAddress = 0x200000u; + refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + bool threw = false; + try { + static_cast(PrepareShader(refused.Request(true))); + } catch (const std::runtime_error&) { + threw = true; + } + ShaderDiskCache::Flush(); + require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored"); + std::uintmax_t planBytes = 0; + for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture)); + std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n"; + const auto run = [&](const char* mode) { + const std::string command = "\"" + std::string(self) + "\" " + mode; + require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed"); + }; + run("--load-plan"); + std::vector entry; + const auto damaged = planEntryPath(fixtures.front()); + require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry"); + entry[entry.size() / 2] ^= std::byte{0x20}; + require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry"); + run("--load-plan-corrupt"); + std::vector rewritten; + std::vector key; + ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key); + IrResourcePlan plan; + require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again"); + run("--load-plan-switch"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -1577,6 +1721,7 @@ int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); + if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); @@ -1592,6 +1737,7 @@ int main(int argc, char** argv) { verifyPlanKey(); verifyStore(); verifyAcrossProcesses(argv[0]); + verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); verifyInvocationIsolation(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 909f3cea13..2e4660e269 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -20,6 +20,7 @@ private: [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request); [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); +[[nodiscard]] std::uint64_t FrontEndBuilds(); [[nodiscard]] std::shared_ptr GetResourcePlan(const RecompileRequest& request); struct SourceEntry; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index aacd8989dd..46f382ce18 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -117,6 +117,8 @@ std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } +std::atomic frontEndBuilds{0}; + } struct PreparedControlFlow { @@ -156,11 +158,16 @@ std::shared_ptr ShaderPreparationContext::AcquireFron return prepared; } +std::uint64_t FrontEndBuilds() { + return frontEndBuilds.load(std::memory_order_relaxed); +} + IrProgram PrepareResourceProgram(const RecompileRequest& request) { return PrepareResourceProgram(request, nullptr); } IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) { + frontEndBuilds.fetch_add(1, std::memory_order_relaxed); ShaderPreparationContext local; const auto frontend = (preparation != nullptr ? *preparation : local).AcquireFrontend(request); const auto stageKind = toShaderStageKind(request.shader.stage); @@ -357,6 +364,14 @@ bool FailureMemo() { return memo; } +bool diskPlans() { + static const bool enabled = [] { + const char* dumpIr = std::getenv("APS5_DUMP_IR"); + return std::getenv("APS5_NO_PERF_DISK_PLANS") == nullptr && (dumpIr == nullptr || *dumpIr == '\0'); + }(); + return enabled && ShaderDiskCache::Enabled() && !DebugProbeActive(); +} + std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) { static PrimedSharedMutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. @@ -400,13 +415,26 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr std::lock_guard lock(source->mutex); if (source->plan == nullptr) { if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); - try { - ResourceProgram resource(request, preparation, reclaim); - source->plan = std::move(resource.plan); - source->program = std::move(resource.program); - } catch (...) { - if (FailureMemo()) source->planFailure = std::current_exception(); - throw; + const bool disk = diskPlans(); + std::vector planKey; + if (disk) { + ShaderDiskCache::BuildPlanKey(request, HostSubgroupSize(request), planKey); + IrResourcePlan loaded; + if (ShaderDiskCache::LoadPlan(planKey, loaded)) { + loaded.shaderHash = request.shader.codeAddress; + source->plan = std::make_shared(std::move(loaded)); + } + } + if (source->plan == nullptr) { + try { + ResourceProgram resource(request, preparation, reclaim); + source->plan = std::move(resource.plan); + source->program = std::move(resource.program); + } catch (...) { + if (FailureMemo()) source->planFailure = std::current_exception(); + throw; + } + if (disk) ShaderDiskCache::StorePlan(std::move(planKey), source->plan); } } } diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 841909dc09..02b6d699a1 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1047,6 +1047,7 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_CODE_HASH_KEY", "APS5_NO_FAILURE_MEMO", "APS5_NO_PERF_FRONTEND_PAIR", + "APS5_NO_PERF_DISK_PLANS", "APS5_NO_PERF_DOM_INTERSECTION", "APS5_NO_PERF_ITERATION_SELECTION", "APS5_NO_RESULT_MEMO", From ed3f2776ed284e35384cfa3423739a6f60ca63d3 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:19:43 +0200 Subject: [PATCH 136/353] fix(recompiler): reject a stored plan whose image table names a missing source DecodePlan checked every descriptor source index a plan holds except the two inside an indirect image table: the heap source and the material source. A damaged entry could therefore decode with a table that points past the plan's descriptor sources, and only the materializer's own bounds checks stood between it and an out-of-range read. The front end interns both sources before the image source that names them, so a plan it builds always passes the new check. agc_shader_disk_cache now requires a plan whose table heap or material source lies past the sources not to decode. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index a732408bce..848a268bfb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1455,6 +1455,8 @@ void verifyMalformedPlans() { rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); + rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; }); + rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; }); auto foreign = handBuiltPlan(); IrValue stray(IrOpcode::Void, IrType::U32, 99u); foreign.srtReads[0].value = &stray; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 02b6d699a1..6ed976c863 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1029,7 +1029,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; - if (!std::ranges::all_of(plan.descriptorSources, [](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size(); })) return false; + if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.indirectImage.has_value() || (knownSource(source.indirectImage->heapSource) && knownSource(source.indirectImage->materialSource))); })) return false; if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; From a69f5e0371a4183cc515d52ebb9cc3836e280a63 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:19:59 +0200 Subject: [PATCH 137/353] test(recompiler): compare stored resource plans field by field The plan tests compared a decoded plan with the original only through the codec itself (re-encoding must give the same bytes) and through materialization. Neither catches a field that EncodePlan and DecodePlan both leave out: the bytes still agree, and the walk does not read every field. Leaving userDataBase, the plan's sRGB formats, the value ids or several MemoryInfo fields (secondaryOffset, dataDwords, dataFormat, planningOnly, idxen) out of both sides passed every test. The hand-built plan also never materializes (its uniform values cannot be evaluated), so its 1000 runtimes all failed with the same error, and no plan held an immediate wider than 32 bits. samePlan compares every field of two plans without the codec: the value graph by storage index (opcode, type, id, flags, register, immediate, arguments, phi blocks), the block ids, the roots, and the plain data through the structures' own equality. The round trip now requires it for the 14 fixtures and the hand-built plan, which sets every MemoryInfo field, all indirect table fields, a second buffer, the whole uniform fill and a 64-bit immediate to values other than their defaults. The second process also builds each fixture's plan with its own front end and requires it to equal the plan it used, so a stored plan must match what the front end builds in another process, not only its own re-encoding. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 140 ++++++++++++++++-- 1 file changed, 126 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 848a268bfb..abc30222b4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1275,18 +1275,58 @@ IrResourcePlan handBuiltPlan() { memory.imageDimension = RdnaImageDimension::Dim2D; memory.gpuDescriptor = true; memory.flushDenormals = true; - plan.memoryInfo = {memory, MemoryInfo{}}; + MemoryInfo every; + every.kind = ResourceKind::Buffer; + every.resource = 2u; + every.sampler = 3u; + every.offset = 0x14u; + every.secondaryOffset = 0x28u; + every.dmask = 0x5u; + every.dataDwords = 3u; + every.dataBits = 16u; + every.componentIndex = 1u; + every.componentCount = 2u; + every.dataFormat = 10u; + every.numberFormat = 7u; + every.imageSampleFlags = 0x21u; + every.imageDimension = RdnaImageDimension::Dim3D; + every.imageAddressComponents = 3u; + every.addressIsFull = true; + every.dataSigned = true; + every.typed = true; + every.formatted = true; + every.d16 = true; + every.imageHasMip = true; + every.imageR128 = true; + every.imagePacked = true; + every.imageByElements = 2u; + every.idxen = true; + every.offen = true; + every.planningOnly = true; + every.coherent = true; + every.gpuDescriptor = true; + every.flushDenormals = true; + plan.memoryInfo = {memory, every, MemoryInfo{}}; DescriptorSource buffer; buffer.dwordCount = 4u; buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer}; DescriptorSource table; table.dwordCount = 8u; table.dwords[0] = &readConst(3); - table.indirectImage = DescriptorSource::IndirectImage{1u, 0u, 16u, 4u, 2u, 32u, true}; - plan.descriptorSources = {buffer, table}; - plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {}, {0u}}, {nullptr, {}, {}, {}}}; - plan.materializationSources = {0u}; - plan.srtReads = {{&readConst(4), 0u}, {&pointer, 1u}}; + table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true}; + DescriptorSource heap; + heap.dwordCount = 4u; + heap.dwords[1] = &readConst(6); + DescriptorSource material; + material.dwordCount = 4u; + material.dwords[2] = &readConst(7); + auto& wide = make(IrOpcode::Void, IrType::U64); + wide.SetImmediateU64(0x8000000000000001ull); + material.dwords[3] = &wide; + plan.descriptorSources = {buffer, table, heap, material}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}}; + plan.materializationSources = {0u, 2u}; + plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; plan.guardedSrtSlots = {1u}; plan.srtGuardOffset = 6u; plan.cleanFlatSlots = {0u, 1u}; @@ -1294,14 +1334,14 @@ IrResourcePlan handBuiltPlan() { plan.requiresSpecializationMemory = true; plan.srtPlanComplete = true; plan.resourceTrackingComplete = true; - plan.info.buffers = {BufferResource{}}; + plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}}; ImageResource image{}; image.source = 1u; image.resourceClass = ImageResourceClass::Sampled; image.dimension = RdnaImageDimension::Dim2D; plan.info.images = {image}; ResourceMaterializer::PrepareImageModes(plan.info); - plan.uniformFill.fill = {UniformFillKind::Buffer, 0u, {4u, 0u, 0u}, 2u, 0u}; + plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u}; auto& fillValue = readConst(5); plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr}; return plan; @@ -1313,6 +1353,71 @@ std::vector encodedPlan(const IrResourcePlan& plan) { return bytes; } +bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { + if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false; + std::map leftValues; + std::map rightValues; + std::map leftBlocks; + std::map rightBlocks; + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + leftValues.emplace(left.valueStorage[index].get(), index); + rightValues.emplace(right.valueStorage[index].get(), index); + } + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + leftBlocks.emplace(left.blockStorage[index].get(), index); + rightBlocks.emplace(right.blockStorage[index].get(), index); + } + const auto sameValue = [&](const IrValue* a, const IrValue* b) { + if (a == nullptr || b == nullptr) return a == b; + const auto foundA = leftValues.find(a); + const auto foundB = rightValues.find(b); + return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second; + }; + const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) { + const auto foundA = leftBlocks.find(a); + const auto foundB = rightBlocks.find(b); + return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second; + }; + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; + } + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + const auto& a = *left.valueStorage[index]; + const auto& b = *right.valueStorage[index]; + if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; + if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; + for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { + if (!sameValue(a.Argument(operand), b.Argument(operand))) return false; + } + for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) { + if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false; + } + } + if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false; + if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false; + for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { + const auto& a = left.descriptorSources[index]; + const auto& b = right.descriptorSources[index]; + if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false; + for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) { + if (!sameValue(a.dwords[dword], b.dwords[dword])) return false; + } + } + for (std::size_t index = 0; index < left.controlFlow.size(); ++index) { + const auto& a = left.controlFlow[index]; + const auto& b = right.controlFlow[index]; + if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false; + } + for (std::size_t index = 0; index < left.srtReads.size(); ++index) { + if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false; + } + for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) { + if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false; + } + return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots && + left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill; +} + struct MaterializedPlan { bool ok = false; std::string error; @@ -1448,10 +1553,10 @@ void verifyMalformedPlans() { }; rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); }); rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; }); - rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 2u; }); + rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; }); rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; }); - rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(2u); }); - rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 2u; }); + rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); }); + rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; }); rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); @@ -1483,7 +1588,7 @@ void verifyPlanRoundTrip() { const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); - require(handDecoded.descriptorSources[1].indirectImage == handBuilt.descriptorSources[1].indirectImage && handDecoded.memoryInfo == handBuilt.memoryInfo && handDecoded.info == handBuilt.info, "the decoded plan lost plain data"); + require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value"); PlanFixture handFixture; handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; std::vector handSrt(16, 0u); @@ -1504,6 +1609,7 @@ void verifyPlanRoundTrip() { IrResourcePlan decoded; require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); + require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); require(succeeded != 0u, fixture.name + ": no runtime materialized"); @@ -1595,7 +1701,7 @@ void appendSnapshot(std::vector& out, const ResourceCapture& capture) appendWords(out, reads); } -std::vector> preparedOutputs(std::vector& fixtures) { +std::vector> preparedOutputs(std::vector& fixtures, std::vector>* plans = nullptr) { std::vector> outputs; for (auto& fixture : fixtures) { const auto request = fixture.Request(true); @@ -1608,6 +1714,7 @@ std::vector> preparedOutputs(std::vector& fi ShaderDiskCache::EncodeResult(*result, bytes); appendSnapshot(bytes, *capture); require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode"); + if (plans != nullptr) plans->push_back(capture->plan); outputs.push_back(std::move(bytes)); } return outputs; @@ -1627,7 +1734,8 @@ int runPlanLoadingProcess(std::string_view mode) { if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); auto fixtures = planFixtures(); const auto count = fixtures.size(); - const auto outputs = preparedOutputs(fixtures); + std::vector> plans; + const auto outputs = preparedOutputs(fixtures, &plans); ShaderDiskCache::Flush(); const auto totals = ShaderDiskCache::Totals(); const auto builds = FrontEndBuilds(); @@ -1659,6 +1767,10 @@ int runPlanLoadingProcess(std::string_view mode) { } else { std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; } + for (std::size_t index = 0; index < count; ++index) { + const auto fresh = GetResourcePlan(fixtures[index].Request(false)); + require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds"); + } ShaderDiskCache::Flush(); return 0; } From 70663fec4247c5d8058170a13135d2dfd1990ca2 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:49:37 +0200 Subject: [PATCH 138/353] test(recompiler): check that APS5_NO_PERF_DISK_PLANS=1 is cache-neutral APS5_NO_PERF_DISK_PLANS=1 is listed as a neutral switch, so it stays out of the variant key, and it exists to compare warm starts with and without stored plans. Nothing tested either property: the cross-process test ran with the switch unset only. A fourth child process now sets the switch against the cache the first process filled. It must run the front end for all 14 fixtures, neither load nor store a plan, still hit all 14 stored variants, and produce the same SPIR-V, bindings, push constants, snapshots, read traces and plans as the first process. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index abc30222b4..4357cb9f09 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1732,6 +1732,7 @@ std::filesystem::path planEntryPath(PlanFixture& fixture) { int runPlanLoadingProcess(std::string_view mode) { if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); + if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1"); auto fixtures = planFixtures(); const auto count = fixtures.size(); std::vector> plans; @@ -1747,6 +1748,8 @@ int runPlanLoadingProcess(std::string_view mode) { } if (mode == "--load-plan") { require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts); + } else if (mode == "--load-plan-off") { + require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts); } else { require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts); } @@ -1764,6 +1767,8 @@ int runPlanLoadingProcess(std::string_view mode) { const auto after = ShaderDiskCache::Totals(); require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan"); std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n"; + } else if (mode == "--load-plan-off") { + std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n"; } else { std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; } @@ -1804,6 +1809,7 @@ void verifyPlanAcrossProcesses(const char* self) { require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed"); }; run("--load-plan"); + run("--load-plan-off"); std::vector entry; const auto damaged = planEntryPath(fixtures.front()); require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry"); From ce8944f853d97fd0ab912b16ea91dce821042fcc Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 17:44:42 +0200 Subject: [PATCH 139/353] test(recompiler): plan an invariant descriptor phi in the disk cache test The round trip requires some fixture to plan a phi, and only one did: the loop counter of "bindless sample in a loop latch", which reaches the plan through the latch condition. The counter is not invariant, so the CPU walk can never evaluate that condition. A plan that keeps only the conditions the walk can evaluate, as #5181 aims to, would hold no phi and fail the check with "no fixture plans a phi". "descriptor invariant phi" loads the image descriptor from the same SRT offset on both sides of a branch on an SRT dword. Its eight dwords are phis of equivalent reads: the tracker keeps them, since it splits only descriptor phis that are not invariant, and the walk evaluates them through the invariant read. The plan stores them as descriptor dwords, whichever conditions it keeps, so the phi encoding stays covered. The fixture goes after the other compute fixtures, so they keep their seeds and their damaged-entry checks. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 4357cb9f09..db7a70c633 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1203,6 +1203,7 @@ std::vector planFixtures() { chained[7] = static_cast(PlanPointerAddress >> 32u); add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast(PlanPayloadAddress), static_cast(PlanPayloadAddress >> 32u)}}}); add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}}); + add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); PlanFixture fragment; fragment.name = "fragment parameters"; From 664ad77037a46e0a17306b182088d3f5306f87bd Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 18:32:19 +0200 Subject: [PATCH 140/353] test(recompiler): pin the field counts of the structures the plan codec writes The plan and ShaderInfo encoders write these structures field by field, and the sizeof asserts are their only reminder to update the encoders. A field that fits in existing padding keeps the size: main's layerExported landed in ShaderInfo with sizeof still 232, and the three bools #5089 adds to DescriptorSource::IndirectImage keep it at 28. A warm cache would then hand back entries without the new field and nothing would fail. Count the fields of every aggregate the plan codec reaches (IrResourcePlan, GuestRegister, MemoryInfo, DescriptorSource and IndirectImage, ResourceBlock, SrtRead, UniformFill, UniformFillPlan, and ShaderInfo with its resource and stage I/O records) by brace-initializing each with more and more convertible-to-anything values until the initializer no longer compiles, and assert the counts. Unlike the size asserts this does not depend on the ABI, so it is checked on every platform. IrValue is a class with private state and keeps only its size assert. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/ShaderDiskCache.cpp | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 6ed976c863..8d934a1716 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -33,6 +33,7 @@ #include #include #include +#include namespace ShaderRecompiler { @@ -110,6 +111,42 @@ static_assert(sizeof(UniformFillPlan) == 64, "UniformFillPlan changed: update th namespace { +struct AnyField { + template + operator TField() const; +}; + +template +constexpr bool bracedWith(std::index_sequence) { + return requires { TStruct{(static_cast(Index), AnyField{})...}; }; +} + +template +constexpr std::size_t fieldCount() { + if constexpr (bracedWith(std::make_index_sequence{})) { + return fieldCount(); + } else { + return Count; + } +} + +static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); +static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); +static_assert(fieldCount() == 37, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 7, "SamplerResource changed: update the info encoder"); +static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); +static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); +static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); +static_assert(fieldCount() == 21, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); +static_assert(fieldCount() == 30, "MemoryInfo changed: update the plan encoder"); +static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); +static_assert(fieldCount() == 7, "IndirectImage changed: update the plan encoder"); +static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); +static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); +static_assert(fieldCount() == 5, "UniformFill changed: update the plan encoder"); +static_assert(fieldCount() == 2, "UniformFillPlan changed: update the plan encoder"); + constexpr std::uint32_t FileMagic = 0x43535041u; struct FileHeader { From 74c3fb657699176081bbbbf96823f3179a8bea11 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 14:14:40 +0200 Subject: [PATCH 141/353] feat(libkernel): implement pthread_setaffinity_np PS5CEMU-HAR (PPSA99360, ps5-payload-dev build, relinks with --trap-sdk-syscall-stubs) imports pthread_setaffinity_np (5KWrg7-ZqvE) from libkernel.prx. babyhuey's run with #3787 merged onto main e7c70a2d (PR #3787, 2026-10-12T10:54:44Z) stops at it: Windows: FAIL: unresolved ELF import 5KWrg7-ZqvE (pthread_setaffinity_np) from libkernel.prx Linux: undefined symbol: 5KWrg7-ZqvE (pthread_setaffinity_np) It follows FreeBSD 11.4 libthr thr_affinity.c (8c639945) over kern_cpuset_setaffinity: another thread is looked up first (EINVAL for null, ESRCH when unknown or finished, as _thr_find_thread without dead threads), a size other than the 32-byte cpuset_t returns ERANGE, the mask is clipped to sceKernelGetAvailableCpumask and an empty result returns EDEADLK, as cpuset_setaffinity does. The affinity is stored on the guest thread through scePthreadSetaffinity, for the caller and for any live guest thread. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Pthread/Posix/Thread.cpp | 15 ++++++++ core/libs/tests/GuestAvailableCpumask.cpp | 35 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 51 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 38eef15ffe..90ad038f81 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -163,6 +163,21 @@ int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpu return 0; } +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset) { + constexpr int GuestEdeadlk = 11; + if (thread != scePthreadSelf()) { + if (!thread) return PosixThread::GUEST_EINVAL; + if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + } + if (cpusetSize != GUEST_CPUSET_BYTES) return GUEST_ERANGE; + if (!cpuset) return PosixThread::GUEST_EFAULT; + KernelCpumask requested = 0; + std::memcpy(&requested, cpuset, sizeof(requested)); + const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); + if (affinity == 0) return GuestEdeadlk; + return PosixThread::ToErrno(scePthreadSetaffinity(thread, affinity)); +} + int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index f15d13e2ec..76b09054a6 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -26,6 +26,7 @@ int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); Pthread APS5_VABI pthread_self_nid_postfix(void); int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset); +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset); int APS5_VABI scePthreadGetthreadid(void); } @@ -46,6 +47,7 @@ struct RestoreProcessMask { static constexpr std::size_t GUEST_CPUSET_BYTES = 32; static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_ERANGE = 34; @@ -236,6 +238,39 @@ int main() { Require(unionResult == 0 && unionSet[0] == (first | last)); Require(ownResult == 0 && ownSet[0] == last); } + std::uint64_t request[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {first, 0, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(pthread_self_nid_postfix()) == first); + std::promise settle; + auto settled = settle.get_future(); + Pthread target = nullptr; + Require(scePthreadCreate(&target, &attr, Worker, &settled, nullptr) == SCE_OK); + request[0] = last; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == last && ReadCpuset(pthread_self_nid_postfix()) == first); + request[0] = available | (1ull << 40); + request[3] = 1ull << 63; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == available); + const std::uint64_t outside[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {1ull << 40, 1, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(ReadCpuset(target) == available && ReadCpuset(pthread_self_nid_postfix()) == first); + for (const std::size_t size : {std::size_t{0}, std::size_t{8}, std::size_t{31}, std::size_t{33}, std::size_t{64}}) { + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, request) == GUEST_ERANGE); + Require(pthread_setaffinity_np_nid_postfix(target, size, request) == GUEST_ERANGE); + } + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, nullptr) == GUEST_EFAULT); + Require(pthread_setaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, request) == GUEST_EINVAL); + std::uint64_t unknownThread[8]{}; + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), 8, request) == GUEST_ESRCH); + settle.set_value(); + Require(scePthreadJoin(target, nullptr) == SCE_OK); + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + request[0] = available; + request[3] = 0; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(ReadCpuset(pthread_self_nid_postfix()) == available); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b54b27aba9..5f2eb97963 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -129,6 +129,7 @@ - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_setaffinity`. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_getaffinity`. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified +- [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. From c60b52d2d8bf2883d9007bc3d38e33c779a3ad3a Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 10 Oct 2026 23:41:02 +1300 Subject: [PATCH 142/353] perf(agc): present resident displays without the 4K convert chain A 10-bit resident display buffer (and an 8-bit one in the other channel order or an sRGB type) went through vkCmdCopyImageToBuffer, the color transfer's convert dispatch, vkCmdCopyBufferToImage into the scaler source and a blit, all at the display's resolution, plus a full clear of the swapchain image. PresentationPass now draws such an image straight into the swapchain image with one fragment pass: it reads the raw texels through an R32_UINT view, decodes them as the guest-memory path does (the display's channel order, the 10-bit variant truncated to 8 bits), scales with a bilinear filter of the decoded values and writes black outside the aspect-fit rectangle, so it needs neither intermediate buffer nor a clear. The swapchain stays B8G8R8A8_UNORM and now also allows color attachment use. A 16-bit float display buffer keeps the convert chain: its encode goes through the scanout threshold tables, and the pass refuses it. The blit paths clear only the letterbox bands (blits from a 1x1 black image) instead of the whole swapchain image, and the scaler source image is only used by the guest-memory, uniform and 16-bit float paths. GPU frame dumps of a drawn image are drawn by the same pass at the dump size, sampling every n-th texel, so they hold the same bytes as before. Test: agc_driver_presentation_pass compares the pass with the convert path for both 8-bit and both 10-bit channel orders (identical unscaled and letterboxed, within two levels of the blit filter scaled), checks that a 10-bit destination keeps every source bit, that dumps equal the subsampled guest-path frame, that the band clears equal a full clear, and that a 16-bit float display buffer is refused. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 47 ++ .../Execution/include/PresentationPass.hpp | 68 +++ .../Execution/include/PresentationScaler.hpp | 6 + .../Execution/src/PresentationPass.cpp | 346 +++++++++++++ .../Execution/src/PresentationScaler.cpp | 80 +++ .../Execution/src/VulkanDevice.cpp | 124 ++--- .../Graphics/shaders/PresentationPass.frag | 53 ++ .../Graphics/shaders/PresentationPass.vert | 6 + .../tests/PresentationPass.cpp | 461 ++++++++++++++++++ 9 files changed, 1134 insertions(+), 57 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert create mode 100644 core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..864c083df6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -108,6 +108,23 @@ add_custom_command( DEPENDS "${agcSampleDumpsShaderSource}" glslang-standalone VERBATIM ) +foreach(agcPresentationStage IN ITEMS Vert Frag) + string(TOLOWER "${agcPresentationStage}" agcPresentationExtension) + string(TOUPPER "${agcPresentationStage}" agcPresentationSymbol) + set(agcPresentationShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PresentationPass.${agcPresentationExtension}) + set(agcPresentationShaderSpv ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}.spv) + set(agcPresentationShaderHeader ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}_spv.h) + add_custom_command( + OUTPUT "${agcPresentationShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcPresentationShaderSpv}" "${agcPresentationShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcPresentationShaderSpv} -DOUTPUT=${agcPresentationShaderHeader} + -DSYMBOL=PRESENTATION_PASS_${agcPresentationSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcPresentationShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -174,6 +191,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp ${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}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -210,6 +228,7 @@ set(agcDriverSources ${agcTextureDetileImageShaderHeader} ${agcSampleCounterShaderHeader} ${agcSampleDumpsShaderHeader} + ${agcPresentationShaderHeaders} ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp @@ -1011,6 +1030,34 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_driver_presentation_pass_tests EXCLUDE_FROM_ALL + tests/PresentationPass.cpp +) + +target_include_directories(agc_driver_presentation_pass_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) + +target_link_libraries(agc_driver_presentation_pass_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) + +set_target_properties(agc_driver_presentation_pass_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_presentation_pass_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_presentation_pass_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_presentation_pass COMMAND agc_driver_presentation_pass_tests) + set_tests_properties(agc_driver_presentation_pass PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_presentation_pass 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/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp new file mode 100644 index 0000000000..2f4a81a3f5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -0,0 +1,68 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +namespace AgcDriver { + +class PresentationPass { +public: + PresentationPass(const Graphics::Context& context, std::size_t slots); + ~PresentationPass(); + PresentationPass(const PresentationPass&) = delete; + PresentationPass& operator=(const PresentationPass&) = delete; + + void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat); + void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout); + void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); + void Release(std::size_t slot) noexcept; + static bool TenBitFormat(VkFormat format); + static VkExtent2D DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride); + +private: + struct Target { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + }; + struct Slot { + VkDescriptorSet set = VK_NULL_HANDLE; + VkImageView source = VK_NULL_HANDLE; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t mode = 0; + std::vector> transient; + VkImage dumpImage = VK_NULL_HANDLE; + VkDeviceMemory dumpMemory = VK_NULL_HANDLE; + VkImageView dumpView = VK_NULL_HANDLE; + VkFramebuffer dumpFramebuffer = VK_NULL_HANDLE; + VkExtent2D dumpExtent{}; + }; + + const Target& target(VkFormat format, VkImageLayout finalLayout); + VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage) const; + VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; + void ensureDumpTarget(Slot& slot, VkExtent2D extent); + void releaseDumpTarget(Slot& slot) noexcept; + void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[8]) const; + void release() noexcept; + + Graphics::Context context; + VkSampler sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::map, Target> targets; + std::vector slots; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index 35e8ed497f..49209743ce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -3,8 +3,10 @@ #define VK_NO_PROTOTYPES #include +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include +#include namespace AgcDriver { @@ -30,6 +32,8 @@ public: // The aspect-fitting blit of RecordBlit from any image: presenting a resident render target needs // no copy into the source image first. static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + void RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static std::vector LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight); std::uint32_t SourceWidth() const { return sourceWidth; } std::uint32_t SourceHeight() const { return sourceHeight; } @@ -42,6 +46,8 @@ private: std::uint32_t sourceHeight = 0; VkImage sourceImage = VK_NULL_HANDLE; VkDeviceMemory sourceMemory = VK_NULL_HANDLE; + VkImage blackImage = VK_NULL_HANDLE; + VkDeviceMemory blackMemory = VK_NULL_HANDLE; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp new file mode 100644 index 0000000000..ace009d1fc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -0,0 +1,346 @@ +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" + +namespace AgcDriver { +namespace { + +constexpr std::uint32_t ModeTenBit = 1; +constexpr std::uint32_t ModeRedLow = 2; +constexpr std::uint32_t ModeKeepTenBit = 4; +constexpr std::uint32_t ModeSubsample = 8; + +} + +PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "presentation pass needs at least one slot"); + try { + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Graphics::Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler presentation pass"); + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &sampler}; + VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + descriptorInfo.bindingCount = 1; + descriptorInfo.pBindings = &binding; + Graphics::Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout presentation pass"); + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0, 32}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &descriptorLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout presentation pass"); + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(slotCount)}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = static_cast(slotCount); + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &size; + Graphics::Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool presentation pass"); + for (auto& slot : slots) { + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = descriptorPool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &descriptorLayout; + Graphics::Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &slot.set), "vkAllocateDescriptorSets presentation pass"); + } + const auto createModule = context.Function("vkCreateShaderModule"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); + moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); + moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV); + moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); + } catch (...) { + release(); + throw; + } +} + +PresentationPass::~PresentationPass() { + release(); +} + +void PresentationPass::release() noexcept { + for (std::size_t i = 0; i < slots.size(); ++i) { + Release(i); + releaseDumpTarget(slots[i]); + } + for (const auto& [key, entry] : targets) { + if (entry.pipeline) context.Function("vkDestroyPipeline")(context.device, entry.pipeline, nullptr); + if (entry.renderPass) context.Function("vkDestroyRenderPass")(context.device, entry.renderPass, nullptr); + } + targets.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (descriptorPool) context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); + if (sampler) context.Function("vkDestroySampler")(context.device, sampler, nullptr); + fragment = vertex = VK_NULL_HANDLE; + descriptorPool = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + descriptorLayout = VK_NULL_HANDLE; + sampler = VK_NULL_HANDLE; +} + +bool PresentationPass::TenBitFormat(VkFormat format) { + return format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; +} + +VkExtent2D PresentationPass::DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride) { + Graphics::Require(stride != 0, "presentation dump stride must be non-zero"); + return {(width + stride - 1) / stride, (height + stride - 1) / stride}; +} + +void PresentationPass::Release(std::size_t index) noexcept { + if (index >= slots.size()) return; + auto& slot = slots[index]; + 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(); + if (slot.source) context.Function("vkDestroyImageView")(context.device, slot.source, nullptr); + slot.source = VK_NULL_HANDLE; +} + +VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImageUsageFlags usage) const { + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = usage; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.pNext = &usageInfo; + viewInfo.image = image; + 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}; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView presentation pass"); + return view; +} + +VkFramebuffer PresentationPass::createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const { + VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + info.renderPass = renderPass; + info.attachmentCount = 1; + info.pAttachments = &view; + info.width = extent.width; + info.height = extent.height; + info.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateFramebuffer")(context.device, &info, nullptr, &framebuffer), "vkCreateFramebuffer presentation pass"); + return framebuffer; +} + +const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImageLayout finalLayout) { + const auto key = std::pair{format, finalLayout}; + if (const auto found = targets.find(key); found != targets.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "presentation pass format is not renderable"); + Target created; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = 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, 0, 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 renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &attachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 2; + renderPassInfo.pDependencies = dependencies; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &renderPassInfo, nullptr, &created.renderPass), "vkCreateRenderPass presentation pass"); + + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + 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.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 pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewport; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.pDynamicState = &dynamic; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = created.renderPass; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &created.pipeline), "vkCreateGraphicsPipelines presentation pass"); + } catch (...) { + if (created.renderPass) context.Function("vkDestroyRenderPass")(context.device, created.renderPass, nullptr); + throw; + } + return targets.emplace(key, created).first->second; +} + +void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat) { + Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); + Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation pass source is unavailable"); + Graphics::Require(!DisplayHalfFloat(pixelFormat), "presentation pass does not decode 16-bit float display buffers"); + Release(index); + auto& slot = slots[index]; + slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT); + slot.width = width; + slot.height = height; + slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u); + const VkDescriptorImageInfo imageInfo{sampler, slot.source, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = slot.set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); +} + +void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[8]) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = entry.renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, entry.pipeline); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &slot.set, 0, nullptr); + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), parameters); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + context.Function("vkCmdEndRenderPass")(commands); +} + +void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); + auto& slot = slots[index]; + const auto& entry = target(format, finalLayout); + const auto view = createView(destination, format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u)); + VkFramebuffer framebuffer = VK_NULL_HANDLE; + try { + framebuffer = createFramebuffer(entry.renderPass, view, extent); + } catch (...) { + context.Function("vkDestroyImageView")(context.device, view, nullptr); + throw; + } + slot.transient.emplace_back(view, framebuffer); + const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); + const std::uint32_t mode = slot.mode | (TenBitFormat(format) ? ModeKeepTenBit : 0u); + const std::int32_t parameters[8] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode)}; + draw(commands, slot, entry, framebuffer, extent, parameters); +} + +void PresentationPass::releaseDumpTarget(Slot& slot) noexcept { + if (slot.dumpFramebuffer) context.Function("vkDestroyFramebuffer")(context.device, slot.dumpFramebuffer, nullptr); + if (slot.dumpView) context.Function("vkDestroyImageView")(context.device, slot.dumpView, nullptr); + if (slot.dumpImage) context.Function("vkDestroyImage")(context.device, slot.dumpImage, nullptr); + if (slot.dumpMemory) context.Function("vkFreeMemory")(context.device, slot.dumpMemory, nullptr); + slot.dumpFramebuffer = VK_NULL_HANDLE; + slot.dumpView = VK_NULL_HANDLE; + slot.dumpImage = VK_NULL_HANDLE; + slot.dumpMemory = VK_NULL_HANDLE; + slot.dumpExtent = {}; +} + +void PresentationPass::ensureDumpTarget(Slot& slot, VkExtent2D extent) { + if (slot.dumpImage != VK_NULL_HANDLE && slot.dumpExtent.width == extent.width && slot.dumpExtent.height == extent.height) return; + releaseDumpTarget(slot); + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {extent.width, extent.height, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &slot.dumpImage), "vkCreateImage presentation dump"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, slot.dumpImage, &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); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &slot.dumpMemory), "vkAllocateMemory presentation dump"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, slot.dumpImage, slot.dumpMemory, 0), "vkBindImageMemory presentation dump"); + slot.dumpView = createView(slot.dumpImage, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + slot.dumpFramebuffer = createFramebuffer(target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL).renderPass, slot.dumpView, extent); + slot.dumpExtent = extent; + } catch (...) { + releaseDumpTarget(slot); + throw; + } +} + +void PresentationPass::RecordDump(VkCommandBuffer commands, std::size_t index, std::uint32_t stride, VkBuffer destination) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE, "presentation dump buffer is unavailable"); + auto& slot = slots[index]; + const auto extent = DumpExtent(slot.width, slot.height, stride); + ensureDumpTarget(slot, extent); + const auto& entry = target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + const std::int32_t parameters[8] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample)}; + draw(commands, slot, entry, slot.dumpFramebuffer, extent, parameters); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {extent.width, extent.height, 1u}; + context.Function("vkCmdCopyImageToBuffer")(commands, slot.dumpImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, ©); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index 354b2f0cbd..2c982db099 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -17,6 +17,8 @@ PresentationScaler::PresentationScaler(const Graphics::Context& context, VkForma PresentationScaler::~PresentationScaler() { release(); + if (blackImage != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, blackImage, nullptr); + if (blackMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, blackMemory, nullptr); } void PresentationScaler::release() noexcept { @@ -155,4 +157,82 @@ void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkComm context.Function("vkCmdBlitImage")(commands, image, layout, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); } +std::vector PresentationScaler::LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto right = rect.x + static_cast(rect.width); + const auto bottom = rect.y + static_cast(rect.height); + std::vector bands; + if (rect.y > 0) bands.push_back({0, 0, destinationWidth, static_cast(rect.y)}); + if (bottom < static_cast(destinationHeight)) bands.push_back({0, bottom, destinationWidth, destinationHeight - static_cast(bottom)}); + if (rect.x > 0) bands.push_back({0, rect.y, static_cast(rect.x), rect.height}); + if (right < static_cast(destinationWidth)) bands.push_back({right, rect.y, destinationWidth - static_cast(right), rect.height}); + return bands; +} + +void PresentationScaler::RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto bands = LetterboxBands(width, height, destinationWidth, destinationHeight); + if (bands.empty()) return; + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + if (blackImage == VK_NULL_HANDLE) { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {1u, 1u, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage letterbox"); + try { + 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); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory letterbox"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory letterbox"); + } catch (...) { + context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + throw; + } + blackImage = image; + blackMemory = memory; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = blackImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + context.Function("vkCmdClearColorImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &barrier.subresourceRange); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + std::vector regions; + regions.reserve(bands.size()); + for (const auto& band : bands) { + VkImageBlit blit{}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcOffsets[1] = {1, 1, 1}; + blit.dstSubresource = blit.srcSubresource; + blit.dstOffsets[0] = {band.x, band.y, 0}; + blit.dstOffsets[1] = {band.x + static_cast(band.width), band.y + static_cast(band.height), 1}; + regions.push_back(blit); + } + context.Function("vkCmdBlitImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5d3ddfec4c..11e32a0fb0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -14,6 +14,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/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -357,6 +358,8 @@ struct VulkanDevice::State { std::shared_ptr>> copiedWriters = std::make_shared>>(); VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; + std::unique_ptr presentation; + VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; // The swapchain image AcquireImage took for the next present(), consumed by that present. bool imageAcquired = false; std::uint32_t acquiredIndex = 0; @@ -540,6 +543,7 @@ struct VulkanDevice::State { // and its statistics taken (under presentMutex). void RetireSlot(PresentSlot& slot) { slot.inFlight = false; + if (presentation) presentation->Release(static_cast(&slot - presentSlots.data())); slot.kept.reset(); if (slot.dumpRecorded) { slot.dumpRecorded = false; @@ -693,6 +697,7 @@ struct VulkanDevice::State { textureCache.reset(); detiler.reset(); colorTransfer.reset(); + presentation.reset(); scaler.reset(); pipelineCache.reset(); context.bufferPool.reset(); @@ -1694,14 +1699,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(getPresentModes(selected, state->surface, &presentModeCount, presentModes.data()), "vkGetPhysicalDeviceSurfacePresentModesKHR"); presentModes.resize(presentModeCount); state->presentMode = SelectPresentMode(std::getenv("ANYPS5_PRESENT_MODE"), presentModes); + require((surface.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0, "surface does not support color attachment images"); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; - swapchain.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; + swapchain.imageFormat = state->surfaceFormat; swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; swapchain.imageExtent = state->extent; swapchain.imageArrayLayers = 1; - swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -1728,7 +1734,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(createFence(state->device, &fence, nullptr, &slot.fence), "vkCreateFence present"); check(state->DeviceFunction("vkAllocateCommandBuffers")(state->device, &allocation, &slot.commands), "vkAllocateCommandBuffers"); } - state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); + state->presentation = std::make_unique(graphicsContext(), state->presentSlots.size()); } } @@ -2374,11 +2381,11 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { VkSwapchainCreateInfoKHR create{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; create.surface = state->surface; create.minImageCount = surface.minImageCount; - create.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; + create.imageFormat = state->surfaceFormat; create.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; create.imageExtent = {width, height}; create.imageArrayLayers = 1; - create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -2700,12 +2707,13 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; + const bool drawn = resident != nullptr && residentConvert && !DisplayHalfFloat(display->pixelFormat); // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them // wait for every slot first. The game path did so before taking the mutex // (PresentWaitsForSlots); this is the fallback. - const bool shared = !clearOnly && !direct; - if ((shared || (!clearOnly && (state->scaler == nullptr || state->scaler->SourceWidth() != width || state->scaler->SourceHeight() != height))) && state->SharedSlotInFlight()) RetirePresents(0); + const bool shared = !clearOnly && !direct && !drawn; + if (shared && state->SharedSlotInFlight()) RetirePresents(0); // The next slot is the oldest; a caller that did not retire it first (PresentPixels) waits here. if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; @@ -2754,8 +2762,10 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu barrier.image = state->images[index]; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); - pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (!drawn) { + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (clearOnly) { APS5_LOG_OUT_DEBUG("Recording swapchain clear opaque=%u image=%p", static_cast(opaque), reinterpret_cast(barrier.image)); VkClearColorValue clear{}; @@ -2763,24 +2773,27 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange); } else { APS5_LOG_OUT_DEBUG("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); - require(state->scaler != nullptr, "presentation scaler is unavailable"); - state->scaler->EnsureSourceImage(width, height); + require(state->scaler != nullptr && state->presentation != nullptr, "presentation scaler is unavailable"); + if (shared) state->scaler->EnsureSourceImage(width, height); + const auto slotIndex = static_cast(&slot - state->presentSlots.data()); VkImageMemoryBarrier residentBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + const VkPipelineStageFlags residentStage = drawn ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT; + const VkAccessFlags residentRead = drawn ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT; if (resident != nullptr) { // The draws and dispatches that produced the image were submitted before this command // buffer; their writes become visible to the blit here. The layout stays GENERAL, the // one the storage image is tracked in. residentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + residentBarrier.dstAccessMask = residentRead; residentBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.image = resident->Image(); residentBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; - pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, residentStage, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (residentConvert) { + if (residentConvert && !drawn) { state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat)); state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); } @@ -2791,56 +2804,54 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } - VkClearColorValue letterbox{}; - letterbox.float32[3] = 1.0f; - state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &letterbox, 1, &barrier.subresourceRange); - VkImageMemoryBarrier letterboxBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - letterboxBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.image = barrier.image; - letterboxBarrier.subresourceRange = barrier.subresourceRange; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &letterboxBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height); - else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); - if (dumpFrame && direct) { - const auto scale = DumpScale(); - slot.dumpWidth = (width + scale - 1) / scale; - slot.dumpHeight = (height + scale - 1) / scale; - slot.dumpScale = 1; - if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); - slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); - slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); - } - if (resident != nullptr) { - // Later batches write the image again (the next frame into this buffer, a refresh): they - // start after the blit has read it. - residentBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (drawn) { + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else { + state->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); + else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - if (!direct) { + const auto scale = DumpScale(); + if (drawn || direct) { + const auto extent = PresentationPass::DumpExtent(width, height, scale); + slot.dumpWidth = extent.width; + slot.dumpHeight = extent.height; + slot.dumpScale = 1; + } else { slot.dumpWidth = state->scaler->SourceWidth(); slot.dumpHeight = state->scaler->SourceHeight(); - slot.dumpScale = DumpScale(); + slot.dumpScale = scale; } const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; if (!slot.dumpBuffer || slot.dumpBuffer->Bytes().size() != dumpBytes) slot.dumpBuffer = std::make_unique(graphicsContext(), dumpBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - (direct ? slot.dumpScaler : state->scaler)->RecordReadback(commands, slot.dumpBuffer->Handle()); + if (drawn) { + state->presentation->RecordDump(commands, slotIndex, scale, slot.dumpBuffer->Handle()); + } else if (direct) { + if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); + slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); + slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } else { + state->scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } + } + if (resident != nullptr) { + residentBarrier.srcAccessMask = residentRead; + residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + pipelineBarrier(commands, residentStage | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = 0; - barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (!drawn) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = 0; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (slot.queries != VK_NULL_HANDLE) state->DeviceFunction("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, slot.queries, 1); check(state->DeviceFunction("vkEndCommandBuffer")(commands), "vkEndCommandBuffer"); APS5_LOG_CHARS_OUT_DEBUG("Presentation command buffer recorded"); @@ -2915,8 +2926,7 @@ bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { VkFilter filter = VK_FILTER_LINEAR; bool pending = false; bool convert = false; - if (PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr || convert) return true; - return state->scaler == nullptr || state->scaler->SourceWidth() != buffer->width || state->scaler->SourceHeight() != buffer->height; + return PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr; } void VulkanDevice::FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag new file mode 100644 index 0000000000..9e60e4e22c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag @@ -0,0 +1,53 @@ +#version 450 + +layout(set = 0, binding = 0) uniform usampler2D source; +layout(push_constant) uniform Parameters { + ivec2 origin; + ivec2 size; + ivec2 sourceSize; + int stride; + uint mode; +} parameters; +layout(location = 0) out vec4 color; + +vec4 decode(uint word) { + uvec4 channels; + float levels = 255.0; + if ((parameters.mode & 1u) != 0u) { + channels = uvec4(word & 0x3ffu, (word >> 10u) & 0x3ffu, (word >> 20u) & 0x3ffu, 0u); + if ((parameters.mode & 4u) != 0u) levels = 1023.0; + else channels >>= 2u; + channels.w = uint(levels); + } else { + channels = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); + } + vec4 value = vec4(channels) / levels; + return (parameters.mode & 2u) != 0u ? value : value.zyxw; +} + +vec4 texel(ivec2 position) { + return decode(texelFetch(source, clamp(position, ivec2(0), parameters.sourceSize - 1), 0).x); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy) - parameters.origin; + if (any(lessThan(pixel, ivec2(0))) || any(greaterThanEqual(pixel, parameters.size))) { + color = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + if ((parameters.mode & 8u) != 0u) { + color = texel(pixel * parameters.stride); + return; + } + if (parameters.size == parameters.sourceSize) { + color = texel(pixel); + return; + } + vec2 position = (vec2(pixel) + 0.5) * vec2(parameters.sourceSize) / vec2(parameters.size) - 0.5; + vec2 base = floor(position); + vec2 weight = position - base; + ivec2 corner = ivec2(base); + vec4 top = mix(texel(corner), texel(corner + ivec2(1, 0)), weight.x); + vec4 bottom = mix(texel(corner + ivec2(0, 1)), texel(corner + ivec2(1, 1)), weight.x); + color = mix(top, bottom, weight.y); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert new file mode 100644 index 0000000000..8b3c0059dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp new file mode 100644 index 0000000000..afa7afa3a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -0,0 +1,461 @@ +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::DisplayBuffer; +using AgcDriver::PresentationPass; +using AgcDriver::PresentationScaler; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; +constexpr std::uint64_t TenBit = 0x0100000000000000ull; +constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; + +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"); + 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; + 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; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + 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{}; +}; + +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 resident present 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 resident present test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory resident present 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 bytesOf(Buffer& buffer, std::size_t size) { + const auto bytes = buffer.Bytes(); + return {bytes.begin(), bytes.begin() + static_cast(size)}; +} + +void letterboxBandTests() { + const std::array, 7> cases{{{3840, 2160, 2261, 1272}, {3840, 2160, 3840, 2160}, {3840, 2160, 1000, 1000}, {3840, 2160, 2000, 500}, {200, 130, 260, 130}, {200, 130, 117, 76}, {7, 3, 5, 11}}}; + for (const auto& [width, height, destinationWidth, destinationHeight] : cases) { + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto bands = PresentationScaler::LetterboxBands(width, height, destinationWidth, destinationHeight); + std::vector covered(static_cast(destinationWidth) * destinationHeight, 0); + auto cover = [&](const AgcDriver::AspectFitRect& area) { + Require(area.width != 0 && area.height != 0, "an empty letterbox band"); + for (std::uint32_t y = 0; y < area.height; ++y) { + for (std::uint32_t x = 0; x < area.width; ++x) ++covered[static_cast(area.y + y) * destinationWidth + area.x + x]; + } + }; + cover(rect); + for (const auto& band : bands) cover(band); + Require(std::all_of(covered.begin(), covered.end(), [](std::uint8_t count) { return count == 1; }), "the letterbox bands and the image do not tile the destination: " + std::to_string(width) + "x" + std::to_string(height) + " into " + std::to_string(destinationWidth) + "x" + std::to_string(destinationHeight)); + } + Require(PresentationScaler::LetterboxBands(3840, 2160, 1920, 1080).empty(), "an exact fit has letterbox bands"); +} + +struct Source { + std::uint64_t pixelFormat; + VkFormat storage; + std::uint32_t width; + std::uint32_t height; + std::vector words; + std::vector decoded; +}; + +Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height) { + Source source{pixelFormat, storage, width, height, std::vector(static_cast(width) * height * 4), {}}; + std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u); + for (auto& byte : source.words) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); + std::vector storageBytes(layout.Bytes() + 65536); + auto* aligned = reinterpret_cast((reinterpret_cast(storageBytes.data()) + 65535u) & ~std::uintptr_t{65535}); + std::span guest(aligned, layout.Bytes()); + layout.Tile(source.words, guest); + const DisplayBuffer display{reinterpret_cast(aligned), pixelFormat, width, height}; + Require(AgcDriver::DisplayBufferSize(display) == guest.size(), "test display buffer size differs from the color layout"); + source.decoded = AgcDriver::DecodeDisplayBuffer(display, guest); + return source; +} + +std::unique_ptr residentImage(const Context& context, const Source& source) { + auto image = std::make_unique(context, source.storage, VkExtent2D{source.width, source.height}, 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, source.words.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), source.words.data(), source.words.size()); + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, image->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 region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {source.width, source.height, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT); + batch.SubmitAndWait(); + return image; +} + +std::vector convertPathPresent(const Context& context, const Source& source, VkExtent2D extent) { + Buffer upload(context, source.decoded.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), source.decoded.data(), source.decoded.size()); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + scaler.EnsureSourceImage(source.width, source.height); + CommandBatch batch(context); + const auto commands = batch.Handle(); + scaler.RecordUpload(commands, upload.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); + 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); + scaler.RecordBlit(commands, destination.Handle(), extent.width, extent.height); + 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(); + return bytesOf(readback, bytes); +} + +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), format, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + return bytesOf(readback, bytes); +} + +std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat); + { + CommandBatch batch(context); + pass.RecordDump(batch.Handle(), 1, stride, readback.Handle()); + batch.SubmitAndWait(); + } + pass.Release(1); + return bytesOf(readback, bytes); +} + +std::string describe(const Source& source, VkExtent2D extent) { + return std::string(AgcDriver::DisplayTenBit(source.pixelFormat) ? "10-bit " : "8-bit ") + (AgcDriver::DisplayRedLow(source.pixelFormat) ? "red-low" : "blue-low") + " display in VkFormat " + std::to_string(static_cast(source.storage)) + " at " + std::to_string(extent.width) + "x" + std::to_string(extent.height); +} + +void passMatchesConvertPath(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const std::array exact{{{source.width, source.height}, {source.width + 60, source.height}, {source.width, source.height + 51}}}; + for (const auto extent : exact) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + Require(expected == drawn, "the presentation pass differs from the convert path: " + describe(source, extent)); + } + const std::array scaled{{{117, 76}, {331, 215}, {150, 150}}}; + for (const auto extent : scaled) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + int worst = 0; + std::size_t differing = 0; + for (std::size_t i = 0; i < expected.size(); ++i) { + const int difference = std::abs(static_cast(expected[i]) - static_cast(drawn[i])); + worst = std::max(worst, difference); + differing += difference != 0; + } + std::printf(" %s: scaled, %zu of %zu channels differ, by at most %d\n", describe(source, extent).c_str(), differing, expected.size(), worst); + Require(worst <= 2, "the scaled presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } +} + +void passDumpMatchesSubsample(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + for (const std::uint32_t stride : {1u, 3u, 4u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(passDump(context, pass, source, resident, stride) == expected, "the presentation dump differs from the subsampled guest-path frame: " + describe(source, extent) + " stride " + std::to_string(stride)); + } +} + +bool renderable(const Context& context, VkFormat format) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; + return (properties.optimalTilingFeatures & needed) == needed; +} + +void passKeepsTenBits(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const bool redLowSource = AgcDriver::DisplayRedLow(source.pixelFormat); + for (const auto format : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + if (!renderable(context, format)) { + std::printf(" 10-bit destination VkFormat %d is not renderable here; skipped\n", static_cast(format)); + continue; + } + const bool redLowDestination = format == VK_FORMAT_A2B10G10R10_UNORM_PACK32; + const VkExtent2D extent{source.width + 40, source.height}; + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(source.width, source.height, extent.width, extent.height); + const auto converted = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, format); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const auto index = static_cast(y) * extent.width + x; + std::uint32_t word = 0; + std::memcpy(&word, drawn.data() + index * 4, 4); + const std::uint32_t low = word & 0x3ffu, green = (word >> 10u) & 0x3ffu, high = (word >> 20u) & 0x3ffu; + const std::uint32_t red = redLowDestination ? low : high, blue = redLowDestination ? high : low; + const bool inside = static_cast(x) >= rect.x && static_cast(x) < rect.x + static_cast(rect.width); + if (!inside) { + Require(word == 0xc0000000u, "a 10-bit letterbox band is not opaque black: " + describe(source, extent)); + continue; + } + std::uint32_t sourceWord = 0; + std::memcpy(&sourceWord, source.words.data() + (static_cast(y) * source.width + (x - static_cast(rect.x))) * 4, 4); + const std::uint32_t sourceLow = sourceWord & 0x3ffu, sourceGreen = (sourceWord >> 10u) & 0x3ffu, sourceHigh = (sourceWord >> 20u) & 0x3ffu; + const std::uint32_t sourceRed = redLowSource ? sourceLow : sourceHigh, sourceBlue = redLowSource ? sourceHigh : sourceLow; + Require(red == sourceRed && green == sourceGreen && blue == sourceBlue && (word >> 30u) == 3u, "the 10-bit presentation lost bits: " + describe(source, extent) + " into VkFormat " + std::to_string(static_cast(format))); + const auto* eight = converted.data() + index * 4; + Require(static_cast(eight[0]) == (blue >> 2u) && static_cast(eight[1]) == (green >> 2u) && static_cast(eight[2]) == (red >> 2u), "the 10-bit presentation's top bits differ from the convert path: " + describe(source, extent)); + } + } + } +} + +void blitLetterboxMatchesClear(const Context& context) { + const auto source = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto resident = residentImage(context, source); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + for (const auto extent : {VkExtent2D{200, 130}, VkExtent2D{260, 130}, VkExtent2D{200, 181}, VkExtent2D{117, 76}, VkExtent2D{331, 300}}) { + const auto bytes = static_cast(extent.width) * extent.height * 4; + std::array, 2> results; + for (std::size_t banded = 0; banded < 2; ++banded) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, bytes, 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); + if (banded != 0) { + scaler.RecordLetterbox(commands, source.width, source.height, destination.Handle(), extent.width, extent.height); + } else { + 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); + } + PresentationScaler::RecordBlitFrom(context, commands, resident->Handle(), VK_IMAGE_LAYOUT_GENERAL, source.width, source.height, VK_FILTER_LINEAR, destination.Handle(), extent.width, extent.height); + 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(); + results[banded] = bytesOf(readback, bytes); + } + Require(results[0] == results[1], "the letterbox bands differ from a full clear: " + describe(source, extent)); + } +} + +void passRefusesHalfFloat(PresentationPass& pass, const Image& resident) { + bool refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgba16Float); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a 16-bit float display buffer"); +} + +void residentPresentTests(const Context& context) { + PresentationPass pass(context, 2); + const std::array, 5> cases{{ + {Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, + {Bgra8, VK_FORMAT_R8G8B8A8_UNORM}, + {Rgba8, VK_FORMAT_B8G8R8A8_UNORM}, + }}; + for (const auto& [pixelFormat, storage] : cases) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passMatchesConvertPath(context, pass, source, *resident); + passDumpMatchesSubsample(context, pass, source, *resident); + if (AgcDriver::DisplayTenBit(pixelFormat)) passKeepsTenBits(context, pass, source, *resident); + if (pixelFormat == Bgra8) passRefusesHalfFloat(pass, *resident); + } + blitLetterboxMatchesClear(context); +} + +} + +int main() { + try { + letterboxBandTests(); + 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; + } + residentPresentTests(device->GetContext()); + std::cout << "resident present format, presentation pass and letterbox tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 04421e7d4dc7bc1e2896bbf7d64cae3f0e7b1d3f Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:43:59 +0500 Subject: [PATCH 143/353] fix(relinker): build the manifest resource in paths with spaces windres passes include directories to the preprocessor unquoted, so relinker.rc failed to compile when the checkout path contains a space. The resource is now an object library without the relinker's include directories. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index e670c9980f..48698704ce 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -116,8 +116,9 @@ if(BUILD_TESTING AND WIN32) endif() if(WIN32) - target_sources(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc) + add_library(relinker_resources OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/relinker.manifest) + target_sources(relinker PRIVATE $) target_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) endif() From cb8b0caf7d712aab66e9b5696174b4afdb7a6ac8 Mon Sep 17 00:00:00 2001 From: Sasha Papo Date: Tue, 13 Oct 2026 10:21:10 -0400 Subject: [PATCH 144/353] fix(libSceNpAuth): report an async request as running on its first poll Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpAuth/Export.cpp | 8 +++++++- core/libs/tests/GuestNpAuth.cpp | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 94a17e4603..86734f9a5a 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -12,6 +12,7 @@ static constexpr int ORBIS_NP_AUTH_ERROR_ABORTED = static_cast(0x80550304); static constexpr int ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550306); static constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ID = static_cast(0x80550307); static constexpr int NP_POLL_ASYNC_FINISHED = 0; +static constexpr int NP_POLL_ASYNC_RUNNING = 1; namespace { @@ -19,6 +20,7 @@ struct RequestInfo { bool isAsync = false; bool processed = false; bool aborted = false; + bool polled = false; int result = SCE_NP_ERROR_SIGNED_OUT; }; @@ -29,7 +31,7 @@ std::atomic g_nextRequest{1}; int CreateRequest(bool isAsync) { const int reqId = g_nextRequest.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_requestMutex); - g_requests[reqId] = {isAsync, false, false, SCE_NP_ERROR_SIGNED_OUT}; + g_requests[reqId] = {isAsync, false, false, false, SCE_NP_ERROR_SIGNED_OUT}; return reqId; } @@ -106,6 +108,10 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.polled) { + it->second.polled = true; + return NP_POLL_ASYNC_RUNNING; + } if (result) *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index f99ceafb24..92cdf6c335 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -27,6 +27,7 @@ int main() { constexpr int aborted = static_cast(0x80550304); constexpr int requestNotFound = static_cast(0x80550306); constexpr int invalidId = static_cast(0x80550307); + constexpr int pollRunning = 1; const int syncReq = sceNpAuthCreateRequest(); const int syncReq2 = sceNpAuthCreateRequest(); @@ -64,6 +65,10 @@ int main() { Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); int asyncPoll = 0; + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == pollRunning); + Require(asyncPoll == 0); + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); + Require(asyncPoll == signedOut); Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); Require(asyncPoll == signedOut); int asyncWait = 0; From f7a2903edfd4ab9e94e59bc0160262119f1802fb Mon Sep 17 00:00:00 2001 From: Sasha Papo Date: Tue, 13 Oct 2026 10:21:11 -0400 Subject: [PATCH 145/353] feat(agc): clip and cull distances on the mesh path Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/ShaderValidation.cpp | 10 ++-- .../execution/VertexClipCullDistances.cpp | 57 ++++++++++++++----- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 50 ++++++++++++++-- 3 files changed, 94 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 25148b44ec..a4d8c14bbc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -115,7 +115,7 @@ struct Module { return; } // One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance. - if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation)) || (value == spv::BuiltInCullDistance && vertex))) { + if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { const auto signature = Signature(type); auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances; Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output"); @@ -250,10 +250,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; - if ((vertex || evaluation) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { + if ((vertex || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { char word[16]; std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl); - Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : vertex && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); + Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : (vertex || mesh) && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); } // Enabled unconditionally or by the device setup in VulkanDevice. @@ -262,12 +262,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || - ((vertex || evaluation) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || capability == spv::CapabilityStorageImageMultisample || - (vertex && shaderCullDistance && capability == spv::CapabilityCullDistance) || + ((vertex || mesh) && shaderCullDistance && capability == spv::CapabilityCullDistance) || capability == spv::CapabilityStorageImageWriteWithoutFormat || capability == spv::CapabilityStorageImageReadWithoutFormat || capability == spv::CapabilityInt64 || diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index fe7d3b448f..5b5a66615f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -25,6 +25,25 @@ alignas(256) constexpr std::array VertexCode{ 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, }; +alignas(256) constexpr std::array MeshCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000c, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80000cf, 0x2f2e2d2c, 0xf80008df, 0x33323130, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +constexpr ShaderRecompiler::MeshConfiguration Subgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + alignas(256) constexpr std::array PixelCode{ 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, }; @@ -78,25 +97,29 @@ bool Expected(Visible visible, std::uint32_t pixel) { return false; } -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row, bool mesh) { Pixels.fill(std::byte{0}); std::array vertices{}; for (std::size_t i = 0; i < vertices.size(); ++i) { vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {row.component0[i], row.component1[i], row.component2[i], 0.0f}}; } - std::vector vertexUserData(4, 0u); + std::vector vertexUserData(mesh ? 12u : 4u, 0u); const auto vertexBuffer = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); - std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin() + (mesh ? 8 : 0)); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const std::span code = mesh ? std::span(MeshCode) : std::span(VertexCode); + const auto stage = mesh ? ShaderStage::Mesh : ShaderStage::Vertex; + const std::array vertexMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; vertexInfo.paClVsOutCntl = row.control; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {stage, reinterpret_cast(code.data()), code, 0, {}}, {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, device.Target(), - {0, 0, 0, 64} + {0, 0, 0, mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : 64u} }; + if (mesh) vertex.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; vertex.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); @@ -115,12 +138,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ - {ShaderStage::Vertex, &vertexResult, 0}, + {stage, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + if (mesh) state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, waveSize, waveSize, Subgroup, std::nullopt}; + else state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; state.paClVsOutCntl = row.control; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; @@ -135,13 +159,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); device.WaitIdle(); } -void Check(const Row& row, std::uint32_t waveSize) { - const auto what = std::string("vertex clip and cull distances, ") + row.name + ", wave" + std::to_string(waveSize); +void Check(const Row& row, std::uint32_t waveSize, bool mesh) { + const auto what = std::string(mesh ? "mesh" : "vertex") + " clip and cull distances, " + row.name + ", wave" + std::to_string(waveSize); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { const auto value = std::to_integer(Pixels[pixel * 4u]); Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); @@ -163,10 +186,18 @@ int main() { } for (const auto waveSize : {64u, 32u}) { for (const auto& row : Rows) { - Draw(*device, waveSize, row); - Check(row, waveSize); + Draw(*device, waveSize, row, false); + Check(row, waveSize, false); } } + if (device->Target().mesh.has_value()) { + for (const auto& row : Rows) { + Draw(*device, 64u, row, true); + Check(row, 64u, true); + } + } else { + std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); + } std::puts("vertex clip and cull distance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index debd3304b6..04480c0caa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -410,16 +410,20 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32); continue; } - const auto storeDistance = [&](std::uint32_t variable, std::uint32_t index) { + const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { if (index == std::numeric_limits::max()) { return; } + if (state.program.Resources().stage == IrShaderStage::Mesh) { + state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, index), f32); + return; + } const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, variable, ConstantU32(state, index)); state.module.AddFunction(spv::OpStore, pointer, f32); }; - storeDistance(state.clipDistanceVariable, output.clipDistance); - storeDistance(state.cullDistanceVariable, output.cullDistance); + storeDistance(StageOutputKind::ClipDistance, state.clipDistanceVariable, output.clipDistance); + storeDistance(StageOutputKind::CullDistance, state.cullDistanceVariable, output.cullDistance); } } @@ -500,8 +504,21 @@ std::uint32_t MeshLoad(SpirvEmitterState& state, std::uint32_t variable, std::ui return value; } +bool IsMeshDistance(StageOutputKind kind) { + return kind == StageOutputKind::ClipDistance || kind == StageOutputKind::CullDistance; +} + std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { - return kind == StageOutputKind::Layer ? TypeU32(state) : TypeF32Vector(state, 4u); + if (kind == StageOutputKind::Layer) { + return TypeU32(state); + } + return IsMeshDistance(kind) ? TypeF32(state) : TypeF32Vector(state, 4u); +} + +std::uint32_t MeshDistancePointer(SpirvEmitterState& state, const SpirvOutputBinding& output, std::uint32_t vertex) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, output.variableId, vertex, ConstantU32(state, output.index)); + return pointer; } } @@ -642,7 +659,30 @@ void DefineMeshOutputs(SpirvEmitterState& state) { throw std::runtime_error("vertex input info is missing for mesh output definition"); } const auto& mesh = state.inputInfo.vertex->mesh; + std::uint32_t clipDistanceCount = 0u; + std::uint32_t cullDistanceCount = 0u; + for (const auto& output : state.outputs) { + if (output.kind == StageOutputKind::ClipDistance) { + clipDistanceCount = std::max(clipDistanceCount, output.index + 1u); + } else if (output.kind == StageOutputKind::CullDistance) { + cullDistanceCount = std::max(cullDistanceCount, output.index + 1u); + } + } for (auto& output : state.outputs) { + if (IsMeshDistance(output.kind)) { + const bool clip = output.kind == StageOutputKind::ClipDistance; + auto& variable = clip ? state.clipDistanceVariable : state.cullDistanceVariable; + if (variable == 0u) { + const auto distances = state.module.Type(spv::OpTypeArray, TypeF32(state), ConstantU32(state, clip ? clipDistanceCount : cullDistanceCount)); + variable = MeshArray(state, spv::StorageClassOutput, distances, mesh.maxVertices); + state.interfaceVariables.push_back(variable); + state.module.AddName(variable, clip ? "gl_ClipDistance" : "gl_CullDistance"); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, clip ? spv::BuiltInClipDistance : spv::BuiltInCullDistance); + } + output.variableId = variable; + output.meshDataVariable = MeshArray(state, spv::StorageClassPrivate, TypeF32(state), state.laneCount); + continue; + } if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer) { throw std::runtime_error("unsupported mesh output kind"); } @@ -727,7 +767,7 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { } const auto type = MeshOutputType(state, output.kind); const auto value = MeshLoad(state, output.meshDataVariable, spv::StorageClassPrivate, type, ConstantU32(state, half)); - const auto pointer = MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); + const auto pointer = IsMeshDistance(output.kind) ? MeshDistancePointer(state, output, index) : MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); state.module.AddFunction(spv::OpStore, pointer, value); } }); From 139bc7b9816939bf9a00905cb12656d387cdcd2d Mon Sep 17 00:00:00 2001 From: ivica00 Date: Mon, 12 Oct 2026 23:34:22 +0200 Subject: [PATCH 146/353] feat(libc): optionally dump indirect memory in Windows crash reports --- .../prx/libc/src/specifics/windows/CrashReport.cpp | 13 +++++++++++++ docs/user/USAGE.md | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index a4671a43eb..f393d809b4 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -282,11 +282,24 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { Report(" r12 %016llx r13 %016llx r14 %016llx r15 %016llx\n", context->R12, context->R13, context->R14, context->R15); // The words each register points at, to recognise which structure a bad pointer came from. { + char indirectBuffer[16]{}; + const bool indirectMemory = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", indirectBuffer, sizeof(indirectBuffer)) > 0 && indirectBuffer[0] != '\0'; const std::pair pointers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; for (const auto& [name, value] : pointers) { if (value < 0x10000 || (value & 7) != 0 || !IsReadable(value) || !IsReadable(value + 0x38)) continue; const auto* words = reinterpret_cast(value); Report(" [%s] %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", name, words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + if (!indirectMemory) continue; + for (std::size_t i = 0; i < 8; ++i) { + const auto address = words[i]; + if (address < 0x10000 || IsExecutable(address)) continue; + std::uint64_t content[8]{}; + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(address), content, sizeof(content), &copied) || copied != sizeof(content)) continue; + Report(" [[%s+0x%llx]] @%016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", + name, static_cast(i * sizeof(*words)), static_cast(address), + content[0], content[1], content[2], content[3], content[4], content[5], content[6], content[7]); + } } } Report(" frame pointer chain:\n"); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4e75ad5559..94682e762e 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -171,6 +171,10 @@ 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. +### 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. + ## Exit codes `0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. From bdded286932bc2c5ec198489204f643c6a24660b Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 13 Oct 2026 20:16:47 +0300 Subject: [PATCH 147/353] chore(readme): add games demo collage to readme --- README.md | 4 +++- demo/web/index.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a585752d17..b07e1abc67 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Tool for automatic executables porting to Linux and Windows. -[**Online converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. +[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. + +![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994) [Discord](https://discord.gg/BHFztBPUe) diff --git a/demo/web/index.html b/demo/web/index.html index 74686f5f1e..c280516fb8 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -2,7 +2,7 @@ - + AnyPS5 · Converter From 6e9fbc0519a41dc58779dab878d38064b8d825e2 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Mon, 12 Oct 2026 23:43:01 +0300 Subject: [PATCH 148/353] fix(libkernel): use precise waits for select timeouts Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 4 +- core/libs/tests/GuestSelectWait.cpp | 73 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestSelectWait.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 72ce92342a..d4a2957957 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1036,6 +1036,16 @@ if(BUILD_TESTING) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_select_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelectWait.cpp) + target_include_directories(guest_select_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_wait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_wait_tests) + add_test(NAME guest_select_wait COMMAND guest_select_wait_tests) + set_tests_properties(guest_select_wait PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select_wait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(NOT WIN32) add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 5082e538ee..9f4b719e5b 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" @@ -11,7 +12,6 @@ #include #include #include -#include #include #include @@ -205,7 +205,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* } if (devicesReady) waitMilliseconds = 0; if (groups.empty()) { - if (waitMilliseconds > 0) std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); + if (waitMilliseconds > 0) PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds))); } else { const int slice = groups.size() == 1 ? waitMilliseconds : std::min(waitMilliseconds, SelectPollSliceMilliseconds); const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); diff --git a/core/libs/tests/GuestSelectWait.cpp b/core/libs/tests/GuestSelectWait.cpp new file mode 100644 index 0000000000..a21b7b320a --- /dev/null +++ b/core/libs/tests/GuestSelectWait.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + using Clock = std::chrono::steady_clock; + for (const std::int64_t micros : {0, 50, 500, 1000, 2000, 16667}) { + const GuestTimeval limit{0, micros}; + for (const int count : {0, 1024}) { + const auto start = Clock::now(); + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &limit) == 0, + "select without descriptors did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(micros), + "select returned before its deadline"); + Require(limit.seconds == 0 && limit.microseconds == micros, + "select changed the guest timeout"); + } + } + const GuestTimeval shortLimit{0, 1000}; + const auto shortWaitStart = Clock::now(); + for (int i = 0; i < 20; ++i) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &shortLimit) == 0, + "repeated select without descriptors did not time out"); + } + const auto shortWait = Clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200), + "twenty 1 ms selects did not stay close to their deadlines"); + for (const GuestTimeval limit : {GuestTimeval{-1, 0}, GuestTimeval{0, -1}, GuestTimeval{0, 1000000}}) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &limit) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid timeout"); + } + const GuestTimeval zero{}; + for (const int count : {-1, 1025}) { + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &zero) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid descriptor limit"); + } + std::array read{}; + read[0] = 1 | (std::uint64_t{1} << 63); + const GuestTimeval limit{0, 500}; + const auto start = Clock::now(); + Require(select_nid_postfix(1, read.data(), nullptr, nullptr, &limit) == 0, + "select with no ready device did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(limit.microseconds), + "select with no ready device returned before its deadline"); + Require(read[0] == (std::uint64_t{1} << 63), + "select did not preserve bits outside the descriptor limit"); + std::array write{}; + write[0] = 2; + const GuestTimeval longLimit{1, 0}; + Require(select_nid_postfix(2, nullptr, write.data(), nullptr, &longLimit) == 1 && write[0] == 2, + "select did not report a ready device"); + std::puts("20 select wait scenarios passed"); +} From 5d3ea5f5119c8e874dc339b0b6059b8a4a56a987 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 19:30:21 +0200 Subject: [PATCH 149/353] test(recompiler): set layerExported in the disk cache entry round trip #5097 added ShaderInfo::layerExported to the entry encoder, and this series moves that line into encodeShaderInfo, which plan entries use too. The entry round trip left the flag false, so an encoder and a decoder that both skipped it still passed. Set it like the other ShaderInfo flags: with layerExported dropped from both, the round trip now fails with "entry round trip: shader info differs". Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index db7a70c633..7fad973a6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -253,6 +253,7 @@ CompiledVariant sampleVariant() { info.info.usesDma = true; info.info.usesFaultBuffer = true; info.info.rasterPrimitive = true; + info.info.layerExported = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; From bcd4c4ed72922c9c10eb934e33f2128c4c1eee58 Mon Sep 17 00:00:00 2001 From: MathiasDevelopes <25400788+MathiasDevelopes@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:59:55 +0200 Subject: [PATCH 150/353] fix(fiber): report the stack used by the executing fiber (cherry picked from commit 807642a29ee88b05ddb38b5925f20c5abe27a5f6) --- core/libs/prx/libSceFiber/Export.cpp | 2 +- core/libs/tests/Fiber.cpp | 21 +++++++++++++++++++++ docs/dev/debt/libSceFiber.md | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 51df883d95..f66a5e5915 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -511,7 +511,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { if (fiber_info->size != sizeof(FiberInfo)) return SCE_FIBER_ERROR_INVALID; fiber_info->entry = reinterpret_cast(fiber->entry); fiber_info->arg_on_initialize = fiber->argOnInitialize; - fiber_info->addr_context = fiber->context; + fiber_info->addr_context = SizedWindow(fiber); fiber_info->size_context = fiber->contextSize; std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index d463848a8a..43ce315163 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -51,10 +51,27 @@ FiberStorage g_secondStorage; auto* const g_first = reinterpret_cast(&g_firstStorage); auto* const g_second = reinterpret_cast(&g_secondStorage); +void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { + FiberInfo info{}; + info.size = sizeof(info); + Require(sceFiberGetInfo(fiber, &info) == 0, "active fiber context info"); + Require(info.size_context == expectedSize, "reported context keeps the requested size"); + volatile std::uint64_t marker = 0; + const auto address = reinterpret_cast(&marker); + const auto low = reinterpret_cast(info.addr_context); + const auto high = low + info.size_context; + Require(address >= low && address < high, "reported fiber context contains the executing stack"); + for (auto cursor = address; cursor < high; cursor += sizeof(std::uint64_t)) { + const volatile auto word = *reinterpret_cast(cursor); + (void)word; + } +} + void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 11 && argOnRun == 100, "first fiber arguments"); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == 0 && self == g_first, "first fiber self"); + CheckActiveContext(g_first, g_firstContext.size()); std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null"); Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer"); @@ -65,6 +82,7 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) std::uint64_t received = 0; Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber"); Require(received == 300 && carried == 1.5, "switch back to first fiber"); + CheckActiveContext(g_first, g_firstContext.size()); for (;;) { Require(sceFiberReturnToThread(received + 1, &received) == 0, "first fiber return"); } @@ -72,10 +90,12 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) void APS5_VABI SecondEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 22 && argOnRun == 200, "second fiber arguments"); + CheckActiveContext(g_second, g_secondContext.size()); Require(sceFiberSwitch(g_second, 0, nullptr) == FiberErrorState, "switch to self"); _mm_setcsr(_mm_getcsr() | 0x8000u); std::uint64_t received = 0; Require(sceFiberReturnToThread(250, &received) == 0 && received == 260, "second fiber resumed on another thread"); + CheckActiveContext(g_second, g_secondContext.size()); Require((_mm_getcsr() & 0x8000u) != 0, "second fiber keeps its MXCSR"); Require(sceFiberSwitch(g_first, 300, nullptr) == 0, "switch to first fiber"); Require(false, "second fiber resumed after its last switch"); @@ -143,6 +163,7 @@ void CheckWindowsHostStack() { #endif void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { + CheckActiveContext(reinterpret_cast(&g_hostCallStorage), HostCallContextSize); #ifdef _WIN32 CheckWindowsHostStack(); #endif diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index bea722d39b..0a64c52ca5 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -1,5 +1,9 @@ # libSceFiber technical debt +### Functional + +- [sceFiberGetInfo](../../../core/libs/prx/libSceFiber/Export.cpp) reports the requested-size window at the top of the host stack used for execution, rather than the original caller buffer, which is retained and pinned. The additional 1 MiB host-call margin below that window is not included in `size_context`. Planet Coaster (PPSA01736 v01.017) uses this range for ChakraCore garbage-collector stack scans; reporting the unused caller buffer makes the scan cross unmapped memory. The range is covered by a regression test while running, switching and migrating a fiber between threads. Console behavior for substituted host stacks has no direct equivalent and is not measured. + ### Unknown function info - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured From ac145e9be48162d75e8509144edbc217dafd41dc Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Tue, 13 Oct 2026 04:54:50 +0300 Subject: [PATCH 151/353] perf(agc): extend pending write snapshots incrementally --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Graphics/include/Recorder.hpp | 4 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 75 +++++--- .../prx/libSceAgcDriver/tests/Recorder.cpp | 161 ++++++++++++++++++ 4 files changed, 225 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..ecc9544646 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -976,6 +976,12 @@ if(BUILD_TESTING) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) + add_test(NAME agc_pending_write_snapshot COMMAND agc_driver_recorder_tests --pending-write-snapshot-only) + set_tests_properties(agc_pending_write_snapshot PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_pending_write_snapshot PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 7e5defb334..1fda73efd9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -741,7 +741,7 @@ private: static const Batch::Read* readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end); bool signaled(const Batch& batch) const; // Rebuilds the lock-free snapshot of pending writes from open, inFlight and finishing. - void publishPendingWrites() const; + void publishPendingWrites(std::span> additions = {}) const; // Appends one range to the open batch; returns whether the snapshot must be rebuilt for it. // `ownLabel`: the range is a label's own store (NoteLabel, AfterCompletions), which does not // overwrite the table entries it covers; any other range flags them (table mutex, briefly). @@ -825,6 +825,8 @@ private: std::map indexRanges; std::list indexRangeRecency; bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); + mutable std::weak_ptr publishedWriteRanges; + mutable bool publishedWriteRangesValid = false; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 192d1e9c3c..22d59b08c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2654,7 +2654,8 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte if (bytes == 0) return; if (&batch == open.get()) { if (!noteWrite(address, bytes, ownLabel)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); return; } @@ -2666,7 +2667,10 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte batch.writeValues.push_back(-1); if (!ownLabel) markOverwritten(address, address + bytes); if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release); - if (!SnapshotCovers(address, address + bytes)) publishPendingWrites(); + if (!SnapshotCovers(address, address + bytes)) { + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); + } std::atomic_thread_fence(std::memory_order_seq_cst); } @@ -2679,7 +2683,8 @@ void Recorder::markOverwritten(std::uint64_t address, std::uint64_t end) { void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { if (!noteWrite(address, bytes)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); // The note precedes this thread's vkQueueSubmit and the label another queue polls for; the // fence makes that order hold without relying on x86 store ordering. std::atomic_thread_fence(std::memory_order_seq_cst); @@ -2687,7 +2692,8 @@ void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { void Recorder::NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value) { if (!noteWrite(address, bytes, false, value)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); } @@ -2697,26 +2703,55 @@ void Recorder::NotePendingWrites(std::span begin && noteWrite(begin, static_cast(end - begin))) publish = true; } if (!publish) return; - publishPendingWrites(); + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); } -void Recorder::publishPendingWrites() const { - // Only the active recorder owns the snapshot: one torn down after its successor was activated, - // or one a worker still dispatches into after device.reset(), must not publish its ranges. - if (activeRecorder != this) return; - const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto merged = std::make_shared(); - if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); - for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - std::sort(merged->begin(), merged->end()); - std::size_t out = 0; - for (const auto& [begin, end] : *merged) { - if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); - else (*merged)[out++] = {begin, end}; +void Recorder::publishPendingWrites(std::span> additions) const { + if (activeRecorder != this) { + publishedWriteRangesValid = false; + return; } - merged->resize(out); + const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const auto previous = pendingWrites.load(std::memory_order_acquire); + const auto owned = publishedWriteRanges.lock(); + const bool extend = !additions.empty() && previous != nullptr && previous == owned && publishedWriteRangesValid && + std::all_of(additions.begin(), additions.end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRangesValid = false; + auto merged = std::make_shared(); + if (extend && additions.size() == 1) { + auto range = additions.front(); + const auto first = std::partition_point(previous->begin(), previous->end(), [&](const auto& entry) { return entry.second < range.first; }); + auto last = first; + while (last != previous->end() && last->first <= range.second) { + range.first = std::min(range.first, last->first); + range.second = std::max(range.second, last->second); + ++last; + } + merged->reserve(previous->size() - static_cast(last - first) + 1); + merged->insert(merged->end(), previous->begin(), first); + merged->push_back(range); + merged->insert(merged->end(), last, previous->end()); + } else { + if (extend) { + merged->reserve(previous->size() + additions.size()); + merged->insert(merged->end(), previous->begin(), previous->end()); + merged->insert(merged->end(), additions.begin(), additions.end()); + } else { + if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); + for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + } + std::sort(merged->begin(), merged->end()); + std::size_t out = 0; + for (const auto& [begin, end] : *merged) { + if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); + else (*merged)[out++] = {begin, end}; + } + merged->resize(out); + } + publishedWriteRangesValid = std::all_of(merged->begin(), merged->end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRanges = merged; pendingWrites.store(std::move(merged), std::memory_order_release); publishGeneration.fetch_add(1, std::memory_order_release); ++snapshotRebuilds; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 815cf01fcb..a818e6facd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -577,6 +578,158 @@ void readTrackingTests(const Device& device, Recorder& recorder) { Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x20000, 0x1000, false), "noted ranges outlived their batch"); } +Recorder::WriteRanges expectedWriteUnion(Recorder::WriteRanges ranges) { + std::sort(ranges.begin(), ranges.end()); + Recorder::WriteRanges result; + for (const auto& [begin, end] : ranges) { + if (!result.empty() && begin <= result.back().second) result.back().second = std::max(result.back().second, end); + else result.emplace_back(begin, end); + } + return result; +} + +void pendingWriteSnapshotTests(const Device& device, Recorder& recorder) { + recorder.Sync(); + Recorder::WriteRanges expected; + const auto check = [&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && *snapshot == expectedWriteUnion(expected), "pending write snapshot differs from the full union"); + }; + const auto note = [&](std::uint64_t address, std::size_t bytes) { + const auto previous = Recorder::PendingWriteSnapshot(); + const auto saved = previous == nullptr ? Recorder::WriteRanges{} : *previous; + recorder.NotePendingWrite(address, bytes); + if (bytes != 0) expected.emplace_back(address, address + bytes); + check(); + Require(previous == nullptr || *previous == saved, "publishing a new write modified an older reader's snapshot"); + }; + note(0x10000, 16); + note(0x10020, 16); + note(0x10010, 16); + note(0x10004, 4); + note(0x10000, 0); + note(0, 1); + note(UINT64_MAX - 16, 16); + const Recorder::WriteRanges additions{{0x20020, 0x20040}, {0x20000, 0x20020}, {0x20010, 0x20030}, {0x20040, 0x20040}, {0x20050, 0x20040}}; + recorder.NotePendingWrites(additions); + for (const auto& range : additions) if (range.second > range.first) expected.push_back(range); + check(); + recorder.NotePendingFill(0x30000, 16, 0xA5); + expected.emplace_back(0x30000, 0x30010); + check(); + alignas(64) static std::array labels{}; + const auto labelAddress = reinterpret_cast(labels.data()); + const std::array value{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + recorder.NoteLabel(labelAddress, value, 1, 0); + expected.emplace_back(labelAddress, labelAddress + value.size()); + check(); + recorder.Submit(); + device.WaitQueue(); + recorder.AfterCompletions(labelAddress + 8, value, 2, 0, true); + expected.emplace_back(labelAddress + 8, labelAddress + 8 + value.size()); + check(); + note(0x40000, 16); + bool completing = false; + recorder.OnComplete([&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(Recorder::SnapshotOverlaps(snapshot.get(), 0x40000, 16), "a finishing batch left the snapshot before its completion"); + completing = true; + }); + recorder.Sync(); + Require(completing && Recorder::PendingWriteSnapshot()->empty(), "completed writes remained published"); + std::mt19937_64 random(0x41505335u); + for (int trial = 0; trial < 16; ++trial) { + expected.clear(); + for (int step = 0; step < 64; ++step) note(0x80000 + random() % 4096, 1 + random() % 128); + recorder.Sync(); + Require(Recorder::PendingWriteSnapshot()->empty(), "random writes survived their GPU batch"); + } + expected.clear(); + note(0x90000, 16); + { + std::atomic invalid{false}; + std::atomic ready{0}; + std::atomic reads{0}; + std::array readers; + for (auto& reader : readers) reader = std::jthread([&](std::stop_token stop) { + ready.fetch_add(1, std::memory_order_release); + while (!stop.stop_requested()) { + const auto snapshot = Recorder::PendingWriteSnapshot(); + if (snapshot == nullptr || !Recorder::SnapshotOverlaps(snapshot.get(), 0x90000, 16)) invalid.store(true, std::memory_order_relaxed); + if (snapshot != nullptr) { + for (std::size_t i = 0; i < snapshot->size(); ++i) { + const auto [begin, end] = (*snapshot)[i]; + if (end <= begin || (i != 0 && (*snapshot)[i - 1].second >= begin)) invalid.store(true, std::memory_order_relaxed); + } + } + reads.fetch_add(1, std::memory_order_relaxed); + } + }); + while (ready.load(std::memory_order_acquire) != readers.size()) std::this_thread::yield(); + for (std::uint64_t i = 0; i < 256; ++i) note(0x91000 + i * 128, 32); + for (auto& reader : readers) reader.request_stop(); + for (auto& reader : readers) reader.join(); + Require(reads.load() != 0 && !invalid.load(), "concurrent snapshot readers observed an invalid or incomplete union"); + } + recorder.Sync(); + expected.clear(); + note(UINT64_MAX - 3, 8); + note(0x50000, 16); + recorder.Sync(); + recorder.NotePendingWrite(0x58000, 16); + { + Recorder other(device.GetContext()); + other.Activate(); + recorder.NotePendingWrite(0x59000, 16); + recorder.Activate(); + recorder.NotePendingWrite(0x5A000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x58000, 0x58010}, {0x59000, 0x59010}, {0x5A000, 0x5A010}}, "reactivation missed writes recorded while inactive"); + other.Activate(); + recorder.Sync(); + recorder.Activate(); + recorder.NotePendingWrite(0x5B000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x5B000, 0x5B010}}, "reactivation retained writes completed while inactive"); + recorder.Sync(); + other.Activate(); + other.NotePendingWrite(0x60000, 16); + const auto otherSnapshot = Recorder::PendingWriteSnapshot(); + recorder.Activate(); + recorder.NotePendingWrite(0x70000, 16); + const auto ownSnapshot = Recorder::PendingWriteSnapshot(); + Require(ownSnapshot != nullptr && *ownSnapshot == Recorder::WriteRanges{{0x70000, 0x70010}}, "a recorder extended another recorder's snapshot"); + Require(*otherSnapshot == Recorder::WriteRanges{{0x60000, 0x60010}}, "a replaced owner's retained snapshot changed"); + other.Sync(); + Require(Recorder::PendingWriteSnapshot() == ownSnapshot, "an inactive recorder replaced the active snapshot"); + recorder.Sync(); + } + Require(Recorder::PendingWriteSnapshot()->empty(), "snapshot ownership tests left pending writes"); + std::cout << "Pending write snapshot differential, lifetime, completion and ownership tests passed\n"; +} + +void pendingWriteSnapshotBenchmark(Recorder& recorder) { + for (std::uint64_t distinct : {8u, 32u, 512u}) { + for (int duplicates : {0, 8, 64}) { + std::array samples{}; + for (auto& sample : samples) { + recorder.Sync(); + static_cast(recorder.Commands()); + const auto start = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < distinct; ++i) { + const auto address = 0x100000 + i * 4096; + recorder.NotePendingWrite(address, 256); + for (int repeat = 0; repeat < duplicates; ++repeat) recorder.NotePendingWrite(address, 256); + } + sample = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && snapshot->size() == distinct, "snapshot benchmark produced another union"); + } + std::sort(samples.begin(), samples.end()); + std::cout << "snapshot-note benchmark: " << distinct << " distinct, " << duplicates << " repeats, median " << samples[2] << " ms\n"; + } + } + recorder.Sync(); +} + void writeSettledTests(const Device& device, Recorder& recorder) { Require(recorder.PendingWriteSettled(0x50000, 0x100), "an unwritten range is not settled"); recorder.NotePendingWrite(0x50000, 0x100); @@ -6154,6 +6307,14 @@ int main(int argc, char** argv) { copiedDirectOrderingTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pending-write-snapshot-only") { + pendingWriteSnapshotTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--benchmark-pending-write-snapshot") { + pendingWriteSnapshotBenchmark(recorder); + return 0; + } if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) { storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only"); return 0; From 2096410b2959132a1d495808a763dc8e1916da13 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 19:44:27 +0300 Subject: [PATCH 152/353] fix(libkernel): report EPERM before EINVAL when writing kern.proc.pathname FreeBSD sysctl_root rejects a write to a read-only node with EPERM before the node handler checks the MIB length, so a write with a short or long kern.proc.pathname MIB fails with EPERM. sysctlbyname for kern.proc.pathname now goes through sysctl with its 3-component MIB so it follows the same order. --- .../prx/libkernel/System/src/SystemConfiguration.cpp | 12 ++++++------ core/libs/tests/GuestSystemConfiguration.cpp | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 672c9d2911..795a4d0199 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -138,14 +138,14 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { - if (nameLength != 4) { - *__error_nid_postfix() = 22; - return -1; - } if (newValue != nullptr) { *__error_nid_postfix() = 1; return -1; } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } const int pid = name[3]; if (pid != -1 && pid != getpid_nid_postfix()) { *__error_nid_postfix() = 3; @@ -184,8 +184,8 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si return -1; } if (std::strcmp(name, "kern.proc.pathname") == 0) { - *__error_nid_postfix() = 22; - return -1; + static constexpr int mib[] = {1, 14, 12}; + return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); } if (std::strcmp(name, "kern.arandom") == 0) { static constexpr int mib[] = {1, 37}; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 1ad0f5c6f7..b960fb85fa 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -127,6 +127,10 @@ static void CheckPathnameSysctl() { const int mibTooLong[] = {1, 14, 12, -1, 0}; Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + const int mibSelf[] = {1, 14, 12, -1}; length = 0; Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); From 286cf28ec7e397432bcd9fa04a0e7d25d071a78f Mon Sep 17 00:00:00 2001 From: thuanquang <60096168+thuanquang@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:17:12 +0700 Subject: [PATCH 153/353] fix(relinker): reject a directory as the executable output path An existing directory, or a path ending in a separator, was only rejected when the executable was written, after the converted modules had already been written to app0/ beside or inside it. Co-Authored-By: Claude Opus 5.5 --- core/relinker/main.cpp | 2 ++ .../relinker/tests/test_input_preservation.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 95652550c3..cf63c3296e 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -91,6 +91,8 @@ int Run(const int argc, char* argv[]) { if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output would overwrite the input executable"); + if (Io::NativePath(args.outputPath).filename().empty() || std::filesystem::is_directory(Io::NativePath(args.outputPath))) + throw Domain::RelinkerException("Executable output is a directory: " + args.outputPath); const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); struct ExtraExecutable { diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 3ec4114b30..10e7c51178 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -142,6 +142,26 @@ def check_registry_after_failed_patch(relinker, work): assert not (case / "app0").exists() +def check_output_directory(relinker, work): + for windows in (False, True): + for existing in (False, True): + case = work / f"output-directory-{windows}-{existing}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + target = case / "output" + if existing: + target.mkdir() + output = str(target) if existing else str(target) + os.sep + options = ["--windows"] if windows else [] + result = subprocess.run([str(relinker), *options, str(source), output], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr) + assert not (case / "app0").exists() and not (target / "app0").exists() + assert target.exists() == existing + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -204,6 +224,7 @@ def main(): check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) check_registry_after_failed_patch(relinker, work) + check_output_directory(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) From 5ca6487ddac253953864287f8039d408a4247a92 Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:02:54 +0100 Subject: [PATCH 154/353] feat(libSceConvertKeycode): implement sceConvertKeycodeGetCharacter for the US layout The signature is inferred from PPSA12361: a 16-bit keycode, a second argument the title passes as 0, the type from sceConvertKeycodeGetImeKeyboardType and a UTF-16 output. The keycode is a USB HID keyboard usage and the unshifted US character is returned; a keycode without a character returns 0. A second argument or keyboard type other than 0 throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceConvertKeycode/Export.cpp | 21 +++++++++++++++++-- docs/dev/debt/libSceConvertKeycode.md | 9 ++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 docs/dev/debt/libSceConvertKeycode.md diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index ec43b33b03..29c849a7e1 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "SceTypes.hpp" #include "HitLog.hpp" #include "prx/libc/include/General.hpp" @@ -6,6 +8,18 @@ namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); + +char16_t UsCharacter(std::uint16_t keycode) { + static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; + static constexpr char16_t kKeypad[] = u"/*-+\0" u"1234567890."; + if (keycode >= 0x04 && keycode <= 0x1D) return static_cast(u'a' + (keycode - 0x04)); + if (keycode >= 0x1E && keycode <= 0x26) return static_cast(u'1' + (keycode - 0x1E)); + if (keycode == 0x27) return u'0'; + if (keycode == 0x2C) return u' '; + if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; + if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; + return 0; +} } // namespace extern "C" { @@ -27,8 +41,11 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { return 0; } -int APS5_VABI sceConvertKeycodeGetCharacter(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { + if (character == nullptr) return kErrInvalidAddress; + if (state != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); + if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); + *character = UsCharacter(keycode); return 0; } diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md new file mode 100644 index 0000000000..1f1b491f21 --- /dev/null +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -0,0 +1,9 @@ +# libSceConvertKeycode technical debt + +### Unknown function info + +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown; a value other than 0 throws, and so does a keyboard type other than 0. + +### Functional + +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. From 15955c089ea89db2ff164bc6c3d9eae26b557f6a Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:53:11 -0400 Subject: [PATCH 155/353] fix(agc): start the sample location registers at their clear-state value A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES > 0 whose context never wrote PA_SC_AA_SAMPLE_LOCS_PIXEL_* (context 0x2fe-0x30d) was rejected with "missing register in context bank at DWORD 0x2fe", because the registers were absent from the initial context (PPSA07411 stops on it after two frames). The MSAA_ENABLE single-sample path read them the same way. The sixteen registers now start at 0, both initially and after CLEAR_STATE. That is the value in libSceAgc's own register defaults (sceAgcGetRegisterDefaults2, every public table that holds context registers, the tables the initial context already follows for PA_SU_VTX_CNTL and PA_SC_CONSERVATIVE_RASTERIZATION_CNTL), in amdgpu's GFX10 clear-state table (clearstate_gfx10.h, gfx10_SECT_CONTEXT_def_8, loaded by gfx_v10_0_cp_gfx_start before CLEAR_STATE) and the GC 10.3 register reset value (gc_10_3_0_default.h). RADV, radeonsi and PAL always write the locations themselves and do not settle the unwritten value. 0 puts every sample at the pixel center. That is not the standard pattern for any sample count, so such a draw decodes as custom locations at the center, as a single-sample scan into a multisampled target already does. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/QueueState.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index f270c94511..fabe4a59a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -240,6 +240,7 @@ inline Registers BuildInitialContextRegisters() { result.emplace(0x31c + 0xfu * i, 0); } for (std::uint32_t i = 0; i < 16; ++i) { + result.emplace(0x2fe + i, 0); result.emplace(0x94 + 2 * i, 0x80000000); result.emplace(0x95 + 2 * i, 0x40004000); result.emplace(0xb4 + 2 * i, 0); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 29a7dc8b2a..3092e67152 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -452,6 +452,58 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnwrittenSampleLocationTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + using AgcDriver::Graphics::SampleLocations; + const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) { + auto written = makeState(); + written.context[0x2f8] = config; + written.context[0x292] = scanMode; + written.context[0x3b8] = 0x0dc00000u; + written.context[0x31d] = attributes; + written.context[0x1b3] = 2; + written.context[0x1b4] = 2; + AgcDriver::QueueState queue; + queue.shader = written.shader; + queue.userConfig = written.userConfig; + written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; }); + return queue; + }; + AgcDriver::QueueState initial; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0"); + for (const auto samples : {2u, 4u, 8u}) { + const auto log2 = static_cast(std::countr_zero(samples)); + auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u)); + const auto state = DecodeState(queue); + Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count"); + Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern"); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center"); + const auto centered = SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection); + } + auto queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + auto state = DecodeState(queue); + Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed"); + queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed"); + const auto custom = SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed"); + queue.ClearContext(); + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS"); + expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples"); + expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples"); + queue = fresh(0, 0, 3); + state = DecodeState(queue); + Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused"); +} + void MsaaEnableSingleSampleTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -5627,6 +5679,7 @@ int main(int argc, char** argv) { MsaaEnableSingleSampleTests(); clipDistanceTests(); framebufferLayerTests(); + UnwrittenSampleLocationTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); From 50b06110bbac6e37fad8c3dcb55fbecac1843134 Mon Sep 17 00:00:00 2001 From: Jesus Date: Tue, 13 Oct 2026 12:40:19 -0500 Subject: [PATCH 156/353] fix(libkernel): keep the Windows vnode watch when the watched file is deleted Windows 10 1903 deletes a file with open handles by moving it into $Extend\$Deleted until the last handle closes, so the next snapshot of a watched file that a title (or the test) deleted saw a new path, tried to watch that directory's changes, got ERROR_ACCESS_DENIED and threw from the watch thread, which aborts the process; the path change was also reported as NOTE_RENAME next to NOTE_DELETE. A snapshot with a pending delete or no links now reports NOTE_DELETE alone and drops the parent directory watch instead of replacing it, as the file has no directory to be renamed or removed from any more. --- .../prx/libkernel/Equeue/src/FileEvent.cpp | 10 ++++++++-- core/libs/tests/GuestKqueue.cpp | 19 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 4540fa0c3b..e3912942b2 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -162,6 +162,10 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { return true; } +bool IsDeleted(const Snapshot& snapshot) { + return snapshot.deletePending || snapshot.links == 0; +} + std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { std::vector hints; if (!directory) { @@ -182,7 +186,7 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& if ((after.deletePending && !before.deletePending) || after.links < before.links) { hints.push_back(NOTE_DELETE); } - if (!after.path.empty() && after.path != before.path) { + if (!after.path.empty() && after.path != before.path && !IsDeleted(after)) { hints.push_back(NOTE_RENAME); } return hints; @@ -276,7 +280,9 @@ void RunHostWatch(std::shared_ptr watch, HostWatch host) { } if (current.path != host.last.path) { CloseChange(host.parent); - host.parent = WatchParent(current.path); + if (!IsDeleted(current)) { + host.parent = WatchParent(current.path); + } } host.last = std::move(current); } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 78c5bd5425..ccbe1d87bf 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -400,6 +400,24 @@ static void DirectoryWatch() { Require(std::filesystem::remove_all(path) == 1); } +static void DeleteWatchedFile() { + const auto path = TemporaryName("deleted"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteWrite | NoteRename)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(unlink_nid_postfix(path.c_str()) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteDelete); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(!std::filesystem::exists(path)); +} + static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -423,6 +441,7 @@ int main() { MoveDirectoryEntries(); #endif DirectoryWatch(); + DeleteWatchedFile(); Timeout(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb36..58d84d13fb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -95,7 +95,7 @@ - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond From 4dd97f7dbe7943278d8106840cb92fab3e25e44e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 16:24:28 +0200 Subject: [PATCH 157/353] feat(libkernel): implement chflags through the guest path resolver PS5MC (PPSA99420, Kodi 22) imports chflags (ixrw0h2tWuI) from libkernel.prx, one reference. Predicted stop once the imports ahead of it (pthread_getthreadid_np, sendfile, wcstok, pause, setrlimit and the termios calls) are covered (not a run): FAIL: unresolved ELF import ixrw0h2tWuI from libkernel.prx The NID is computed with tools/nid_names.py and matches aerolib. Follows FreeBSD 11.4 (8c639945a44588f6cfdddfb057bb5aad88735da0) sys/kern/vfs_syscalls.c kern_chflagsat and setfflags, and sys/ufs/ufs/ufs_vnops.c ufs_setattr, for a root caller: the lookup through the guest resolver gives EFAULT, ENOENT and ENOTDIR (also for a file reached through a trailing slash or used as a directory), a bit outside the 17 UFS flags of sys/sys/stat.h (VNOVAL included) gives EOPNOTSUPP, and SF_SNAPSHOT gives EPERM, since the snapshot flag cannot be toggled. No host keeps BSD file flags and stat reports st_flags 0, so flags 0 succeed without touching the file and any other flag throws with the flags named. UF_READONLY and the other Windows-attribute flags are not mapped to FILE_ATTRIBUTE_READONLY and its siblings: UFS stores them without enforcing them, and on Windows the read-only attribute is already the owner write bit of chmod. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 16e5b7ccd04e6467a5a282b6cfc9271304f42244) --- core/libs/prx/libkernel/File/src/Stdio.cpp | 42 ++++++++++++++++++++++ core/libs/tests/GuestFilesystem.cpp | 23 ++++++++++++ docs/dev/debt/libkernel.md | 5 +++ 3 files changed, 70 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d9ceb8756b..f94bb212c6 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include struct KernelIovec { @@ -286,6 +287,7 @@ static const struct iovec* NativeIovecs(const KernelIovec* iov) { } #endif +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_ENOENT = 2; static constexpr int GUEST_EIO = 5; static constexpr int GUEST_EBADF = 9; @@ -297,6 +299,7 @@ static constexpr int GUEST_ENAMETOOLONG = 63; static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; +static constexpr int GUEST_EOPNOTSUPP = 45; static constexpr int GUEST_ENOTEMPTY = 66; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_ELOOP = 62; @@ -478,6 +481,33 @@ int File::DuplicateFrom(int fd, int start) { #endif } +static int LookupError(const std::filesystem::path& native) { + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (std::filesystem::exists(status)) return native.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; + if (status.type() != std::filesystem::file_type::not_found) return SceErrorFromErrno(error.default_error_condition().value()) & 0xffff; + const bool fileWithSeparator = !native.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(native), error); + return fileWithSeparator || HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT; +} + +static constexpr std::pair GUEST_FILE_FLAGS[] = { + {0x00000001, "UF_NODUMP"}, {0x00000002, "UF_IMMUTABLE"}, {0x00000004, "UF_APPEND"}, {0x00000008, "UF_OPAQUE"}, + {0x00000010, "UF_NOUNLINK"}, {0x00000080, "UF_SYSTEM"}, {0x00000100, "UF_SPARSE"}, {0x00000200, "UF_OFFLINE"}, + {0x00000400, "UF_REPARSE"}, {0x00000800, "UF_ARCHIVE"}, {0x00001000, "UF_READONLY"}, {0x00008000, "UF_HIDDEN"}, + {0x00010000, "SF_ARCHIVED"}, {0x00020000, "SF_IMMUTABLE"}, {0x00040000, "SF_APPEND"}, {0x00100000, "SF_NOUNLINK"}, + {0x00200000, "SF_SNAPSHOT"}}; +static constexpr std::uint64_t GUEST_SF_SNAPSHOT = 0x00200000; + +static std::string FileFlagNames(std::uint64_t flags) { + std::string names; + for (const auto& [flag, name] : GUEST_FILE_FLAGS) { + if ((flags & flag) == 0) continue; + if (!names.empty()) names += '|'; + names += name; + } + return names; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -488,6 +518,18 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { return 0; } +int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { + if (const int error = PathError(path)) return PosixFailure(error); + if (const int error = LookupError(ResolvePath_nid_no_patch(path))) return PosixFailure(error); + std::uint64_t known = 0; + for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; + if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); + if ((flags & GUEST_SF_SNAPSHOT) != 0) return PosixFailure(GUEST_EPERM); + if (flags != 0) + throw std::runtime_error("chflags: setting " + FileFlagNames(flags) + " on " + path + " is not supported, the host keeps no BSD file flags"); + return 0; +} + int APS5_VABI close_nid_postfix(int d) { int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 987e09890d..e86761e7bf 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -93,6 +93,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags, const char* names) { + try { + chflags_nid_postfix(path.string().c_str(), flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -329,6 +338,20 @@ int main() { Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; { std::ofstream stream(file); stream << "retained until removal"; } + Require(chflags_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + Require(chflags_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((file / "child").string().c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix((file.string() + "/").c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix(file.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(root.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(file.string().c_str(), 0x20) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); + Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb36..4c09ffbd89 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -169,3 +169,8 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known +<<<<<<< HEAD +- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up +======= +- [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +>>>>>>> cf419bd17 (feat(libkernel): implement chflags through the guest path resolver) From 8b5cfb9850a8880ddd05af7d65dac196d08185ea Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 08:20:18 +0000 Subject: [PATCH 158/353] test(libkernel): declare chflags guest entry point --- core/libs/tests/GuestFilesystem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index e86761e7bf..188f1595fe 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -27,6 +27,7 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelFchmod(int, unsigned short); int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); +int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); From dfc9223248ac86ce49e2cdcdc1bdcc775c13aa21 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 18:54:49 +0000 Subject: [PATCH 159/353] docs(libkernel): retain chflags debt after rebase --- docs/dev/debt/libkernel.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4c09ffbd89..7771462768 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -169,8 +169,4 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known -<<<<<<< HEAD -- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up -======= - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled ->>>>>>> cf419bd17 (feat(libkernel): implement chflags through the guest path resolver) From 7b70941f554e99db5eced25f20937a89cae59173 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:48:59 +0200 Subject: [PATCH 160/353] fix(relinker): retain imports used by sectionless exports --- .../relinker/domain/IUnusedNidFilter.hpp | 3 +- .../relinker/src/analysis/UnusedNidFilter.cpp | 5 +- .../UnusedNidFilter/StrictNidFilter.cpp | 2 +- .../src/pipeline/RelinkerPipeline.cpp | 56 ++++++++++++------- .../tests/test_unused_filter_indirect.py | 38 ++++++++++++- 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp index 98bb86c9de..f2c5ea47a7 100644 --- a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp +++ b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp @@ -14,7 +14,8 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries = {} ) = 0; }; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp index 1426881a92..3dbe4ab5c4 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -13,13 +13,16 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries ) override { if (textSection.empty()) throw RelinkerException("Cannot filter NIDs: text section is empty"); auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes); auto collector = UnusedNidFilter::MakeEntryPointCollector(); auto entries = collector->Collect(elfBytes, textVAddr, textSection.size(), *relativeRelocations); + for (const auto entry : extraEntries) + if (entry >= textVAddr && entry - textVAddr < textSection.size()) entries.push_back(entry); if (entries.empty()) throw RelinkerException("Cannot filter NIDs: no entry points found"); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 2fb2cc1158..c2a24c806f 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -192,7 +192,7 @@ class StrictNidFilter : public IUnusedNidFilter { public: explicit StrictNidFilter(Domain::IProgress& progress) : progress(progress) {} - std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr) override { + std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr, const std::vector&) override { const StrictImage image(elfBytes); const auto input = image.Build(nidRefs, textSection, textVAddr); const auto analysis = UnusedNidFilter::AnalyzeStrictReachability(input); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index bd98088047..16f8b87797 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Relinker { @@ -327,6 +328,31 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf validationPolicy->ValidateSyscallAbsence(); + std::vector> exportedSymbols; + std::vector exportedFunctions; + if (hasTag(DT_OS_SYMTABSZ)) { + const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); + if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) + throw RelinkerException("Invalid executable dynamic symbol table size"); + for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + std::uint16_t section = 0; + std::memcpy(§ion, symbol + 6, sizeof(section)); + if (section == 0 || (symbol[4] >> 4) == 0 || (symbol[5] & 3) == 1 || (symbol[5] & 3) == 2) continue; + std::uint32_t sourceName = 0; + std::memcpy(&sourceName, symbol, sizeof(sourceName)); + auto name = readCStr(sourceName); + name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); + exportedSymbols.emplace_back(off, std::move(name)); + const auto binding = symbol[4] >> 4; + if ((binding == STB_GLOBAL || binding == STB_WEAK) && (symbol[4] & 15) == STT_FUNC) { + VirtualAddress address = 0; + std::memcpy(&address, symbol + 8, sizeof(address)); + exportedFunctions.push_back(address); + } + } + } + progress.Report({Domain::ProgressStage::Imports, {}, 0, 1}); const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; @@ -354,7 +380,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf { std::ostringstream message; message << "CFG/GOT filtering skipped: multiple executable segments are not modeled; " << nonPltCount << " -> " << nonPltCount << "; filtered=0\n"; progress.Message(message.str()); } } else { - nonPltRefs = unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr); + nonPltRefs = unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr, exportedFunctions); if (nonPltRefs.size() > nonPltCount) throw RelinkerException("Unused NID filter increased the reference count"); { std::ostringstream message; message << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; progress.Message(message.str()); } } @@ -385,26 +411,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (auto declared = Domain::DeclaredImportModule(ref.Nid, importModules); !declared.empty()) dynSection.UnmatchedImportModules.insert(std::move(declared)); } - if (hasTag(DT_OS_SYMTABSZ)) { - const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); - if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) - throw RelinkerException("Invalid executable dynamic symbol table size"); - for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { - const auto* symbol = raw.data() + dynSymTabOffset + off; - std::uint16_t section = 0; - std::memcpy(§ion, symbol + 6, sizeof(section)); - if (section == 0 || (symbol[4] >> 4) == 0 || (symbol[5] & 3) == 1 || (symbol[5] & 3) == 2) continue; - std::uint32_t sourceName = 0; - std::memcpy(&sourceName, symbol, sizeof(sourceName)); - auto name = readCStr(sourceName); - name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); - const auto nameOffset = static_cast(dynSection.DynStrData.size()); - dynSection.DynStrData.insert(dynSection.DynStrData.end(), name.begin(), name.end()); - dynSection.DynStrData.push_back(0); - const auto position = dynSection.DynSymData.size(); - dynSection.DynSymData.insert(dynSection.DynSymData.end(), symbol, symbol + symEntSize); - std::memcpy(dynSection.DynSymData.data() + position, &nameOffset, sizeof(nameOffset)); - } + for (const auto& [off, name] : exportedSymbols) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + const auto nameOffset = static_cast(dynSection.DynStrData.size()); + dynSection.DynStrData.insert(dynSection.DynStrData.end(), name.begin(), name.end()); + dynSection.DynStrData.push_back(0); + const auto position = dynSection.DynSymData.size(); + dynSection.DynSymData.insert(dynSection.DynSymData.end(), symbol, symbol + symEntSize); + std::memcpy(dynSection.DynSymData.data() + position, &nameOffset, sizeof(nameOffset)); } auto appendRela = [&](std::vector& buf, std::uint64_t offset, std::uint64_t info, std::int64_t addend) { diff --git a/core/relinker/relinker/tests/test_unused_filter_indirect.py b/core/relinker/relinker/tests/test_unused_filter_indirect.py index f3fd080ab4..7528f0434b 100644 --- a/core/relinker/relinker/tests/test_unused_filter_indirect.py +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -51,20 +51,44 @@ def load_slot(address): return b"\x48\x8b\x05" + rel(address + 7, SLOT) + b"\xc3" -def run(relinker, work, name, image): +def run(relinker, work, name, image, level=1): source = work / (name + ".elf") output = work / (name + ".out") source.write_bytes(image) - result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", "unused-filter=1", + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0 and output.exists(), (name, result.returncode, result.stdout, result.stderr) registry = json.loads(output.with_suffix(".registry.json").read_text()) kept = len(registry) assert kept in (0, 1) and all(entry["nid"] == "foo" for entry in registry), (name, registry) - assert f"CFG/GOT filtering: 1 -> {kept};" in result.stdout, (name, result.stdout) + if level == 1: + assert f"CFG/GOT filtering: 1 -> {kept};" in result.stdout, (name, result.stdout) return kept == 1 +def exported_fixture(binding, defined, sections): + image = build(at_offset([(0, b"\xc3"), (0x40, load_slot(FUNCTION))])) + strings = b"\0foo\0lib.so\0exported\0" + image[0x600:0x600 + len(strings)] = strings + struct.pack_into(" Date: Mon, 12 Oct 2026 18:11:49 -0400 Subject: [PATCH 161/353] fix(tools): validate empty ELF dynamic symbol table bounds --- tools/import_audit.py | 1 + tools/tests/test_import_audit.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/tools/import_audit.py b/tools/import_audit.py index f0fd82f2d2..eaa65f225d 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -157,6 +157,7 @@ def elf_exports(path, data): continue if entry != 24 or size % 24 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})") + take(data, offset, size, path, "dynamic symbol table") strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") indexes = [candidate for candidate in sections if candidate[1] == 18 and candidate[6] == section_index] diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 1e84d84ea6..07e1e9f98f 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -102,6 +102,13 @@ def exporting(*names): return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names]) +def with_elf_symbol_table_range(image, offset, size): + image = bytearray(image) + section_offset = struct.unpack_from(" Date: Tue, 13 Oct 2026 12:33:24 +0000 Subject: [PATCH 162/353] fix(shader): read both version 21 request layouts --- CMakeLists.txt | 8 +++++ .../ControlFlow/src/RequestSerializer.cpp | 12 ++++++- .../tests/RequestSerializerTests.cpp | 33 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 core/shader/recompiler/tests/RequestSerializerTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3eb8dd5333..8704dfdbd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler) add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests) set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20) + add_test_executable(request_serializer_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp) + target_include_directories(request_serializer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include) + add_test(NAME request_serializer COMMAND request_serializer_tests) + set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 217d54eee7..eb3c709704 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -153,6 +153,8 @@ public: bool ReadBool() { return ReadU8() != 0u; } + std::uint64_t Remaining() const { return m_data.size() - m_offset; } + std::uint32_t ReadU32() { std::uint32_t value = 0u; for (std::uint32_t i = 0; i < 4u; i++) { @@ -808,7 +810,15 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); - if (version >= 21u) result.request.target.float16DenormPreserve = reader.ReadBool(); + if (version >= 21u) { + if (reader.Remaining() == 1u) { + const auto float16DenormPreserve = reader.ReadU8(); + if (float16DenormPreserve > 1u) throw std::runtime_error("RequestSerializer: invalid float16DenormPreserve value"); + result.request.target.float16DenormPreserve = float16DenormPreserve != 0u; + } else if (reader.Remaining() != 0u) { + throw std::runtime_error("RequestSerializer: unexpected trailing data"); + } + } return result; } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp new file mode 100644 index 0000000000..9066ce55c9 --- /dev/null +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -0,0 +1,33 @@ +#include "ControlFlow/RequestSerializer.hpp" + +#include +#include + +namespace { + +constexpr char kHistoricalVersion21[] = + "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; + +void Require(bool condition) { + if (!condition) std::abort(); +} + +} + +int main() { + const ShaderRecompiler::RequestSerializer serializer; + + const auto historical = serializer.Deserialize(kHistoricalVersion21); + Require(!historical.request.target.float16DenormPreserve); + + ShaderRecompiler::RecompileRequest request{}; + request.target.float16DenormPreserve = false; + const auto withoutPreserve = serializer.Deserialize(serializer.Serialize(request)); + Require(!withoutPreserve.request.target.float16DenormPreserve); + + request.target.float16DenormPreserve = true; + const auto withPreserve = serializer.Deserialize(serializer.Serialize(request)); + Require(withPreserve.request.target.float16DenormPreserve); + + return 0; +} From 0e087e974e62aa884d650b98447865ed8f59159d Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 12:46:15 +0000 Subject: [PATCH 163/353] test(shader): cover version 21 F16 request layout --- core/shader/recompiler/tests/RequestSerializerTests.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 9066ce55c9..28bb1eae0b 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -7,6 +7,8 @@ namespace { constexpr char kHistoricalVersion21[] = "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; +constexpr char kVersion21WithF16Tail[] = + "NVNQQRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAEB"; void Require(bool condition) { if (!condition) std::abort(); @@ -20,6 +22,9 @@ int main() { const auto historical = serializer.Deserialize(kHistoricalVersion21); Require(!historical.request.target.float16DenormPreserve); + const auto version21WithTail = serializer.Deserialize(kVersion21WithF16Tail); + Require(version21WithTail.request.target.float16DenormPreserve); + ShaderRecompiler::RecompileRequest request{}; request.target.float16DenormPreserve = false; const auto withoutPreserve = serializer.Deserialize(serializer.Serialize(request)); From 2ad825f6590e3e94bd836af5ca02db6199be315e Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 13:31:35 +0000 Subject: [PATCH 164/353] fix(shader): require the version 22 F16 request flag --- .../recompiler/ControlFlow/src/RequestSerializer.cpp | 1 + .../recompiler/tests/RequestSerializerTests.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index eb3c709704..8cf344e0b0 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -811,6 +811,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); if (version >= 21u) { + if (version >= 22u) reader.Require(1u); if (reader.Remaining() == 1u) { const auto float16DenormPreserve = reader.ReadU8(); if (float16DenormPreserve > 1u) throw std::runtime_error("RequestSerializer: invalid float16DenormPreserve value"); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 28bb1eae0b..18b926c5ec 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace { @@ -9,6 +10,8 @@ constexpr char kHistoricalVersion21[] = "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; constexpr char kVersion21WithF16Tail[] = "NVNQQRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAEB"; +constexpr char kVersion22WithoutF16Tail[] = + "NVNQQRYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAE="; void Require(bool condition) { if (!condition) std::abort(); @@ -25,6 +28,14 @@ int main() { const auto version21WithTail = serializer.Deserialize(kVersion21WithF16Tail); Require(version21WithTail.request.target.float16DenormPreserve); + bool rejectedTruncatedVersion22 = false; + try { + static_cast(serializer.Deserialize(kVersion22WithoutF16Tail)); + } catch (const std::runtime_error& error) { + rejectedTruncatedVersion22 = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejectedTruncatedVersion22); + ShaderRecompiler::RecompileRequest request{}; request.target.float16DenormPreserve = false; const auto withoutPreserve = serializer.Deserialize(serializer.Serialize(request)); From e5c782c54fe63b983d68e1430e31cc872e0cd41d Mon Sep 17 00:00:00 2001 From: Leandro Soares Date: Sat, 10 Oct 2026 22:04:35 -0300 Subject: [PATCH 165/353] fix(relinker): load converted Windows executables at a fixed low base --- .../elfpatcher/windows/WindowsPeWriter.hpp | 2 +- .../src/windows/WindowsPePatcher.cpp | 2 +- .../src/windows/WindowsPeWriter.cpp | 4 +- .../tests/test_windows_address_space.py | 47 +++++++++++++++++-- docs/dev/debt/relinker.md | 1 + 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index 9c0b8f38f6..db44e19058 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -8,7 +8,7 @@ namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt, bool dynamicBase = true) const; }; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 2b4faa6bf5..dfec5f91d8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -110,7 +110,7 @@ std::vector WindowsPePatcher::Patch(const std::vector(image.GetImageBase()) : std::nullopt); + auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional(image.GetImageBase()) : std::nullopt, false); report(10000); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 1e346dbeda..0b64a5a2d5 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase) const { +std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase, const bool dynamicBase) const { constexpr std::size_t peOffset = 0x80; constexpr std::size_t optionalOffset = peOffset + 24; constexpr std::size_t sectionTable = optionalOffset + 240; @@ -32,7 +32,7 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, optionalOffset + 50, 2); Io::WriteU32(result, optionalOffset + 60, LoadRva); Io::WriteU16(result, optionalOffset + 68, windowsGui ? 2 : 3); - Io::WriteU16(result, optionalOffset + 70, directories[5].Size == 0 ? 0x100 : 0x140); + Io::WriteU16(result, optionalOffset + 70, dynamicBase && directories[5].Size != 0 ? 0x140 : 0x100); Io::WriteU64(result, optionalOffset + 72, 0x210000); Io::WriteU64(result, optionalOffset + 80, 0x200000); Io::WriteU64(result, optionalOffset + 88, 0x100000); diff --git a/core/relinker/relinker/tests/test_windows_address_space.py b/core/relinker/relinker/tests/test_windows_address_space.py index d2720daf6b..8ad7e7f792 100644 --- a/core/relinker/relinker/tests/test_windows_address_space.py +++ b/core/relinker/relinker/tests/test_windows_address_space.py @@ -1,4 +1,4 @@ -"""Keep the main thread stack that Windows creates before libc reserves the guest arena below it.""" +"""Keep converted Windows images inside the console's 40-bit address window: the executable loads at its fixed low base, guest modules keep relocating, and the main thread stack that Windows creates before libc reserves the guest arena stays below it.""" import os from pathlib import Path @@ -7,6 +7,7 @@ import subprocess import sys import tempfile +from test_guest_module_directories import module_with_symbol from test_optional_plt import fixture GUEST_ARENA_START = 0x200000000 @@ -14,9 +15,28 @@ DYNAMIC_BASE = 0x40 NX_COMPAT = 0x100 -def dll_characteristics(pe): +def optional_header(pe): pe_offset = struct.unpack_from(" 0, "executable fixture has no base relocations" + assert dll_characteristics(executable) == NX_COMPAT, f"DllCharacteristics {dll_characteristics(executable):#x}" + assert image_base(executable) < GUEST_ARENA_START, f"ImageBase {image_base(executable):#x}" if os.name == "nt": for _ in range(8): executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode < GUEST_ARENA_START >> 32, \ f"stack not below {GUEST_ARENA_START:#x}: exit code {executed.returncode:#x}" + module_case = work / "module" + (module_case / "prx").mkdir(parents=True) + (module_case / "prx" / "provider.prx").write_bytes(module_with_relative_relocation()) + module_source = module_case / "input.elf" + module_source.write_bytes(fixture()) + module_output = module_case / "output.exe" + result = subprocess.run([str(relinker), "--windows", str(module_source), str(module_output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = module_case / "app0" / "prx" / "provider.prx.guest.prx" + assert artifact.is_file(), list((module_case / "app0").rglob("*")) + module = artifact.read_bytes() + assert relocation_directory_size(module) > 0, "module fixture has no base relocations" + assert dll_characteristics(module) == DYNAMIC_BASE | NX_COMPAT, \ + f"DllCharacteristics {dll_characteristics(module):#x}" print("Windows address space integration tests passed") diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index a77abb414c..a74958d0ed 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,6 +4,7 @@ - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). +- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) so they can load at the fixed low image base inside the console 40-bit address window; Windows 10 PRAGMATA evidence is recorded in #3043, but this behavior has not been verified on console hardware or other Windows versions. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - `--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. From df32a44f7990b215cc9ea9327000670a16b125ad Mon Sep 17 00:00:00 2001 From: ps54all Date: Tue, 13 Oct 2026 20:44:40 +0200 Subject: [PATCH 166/353] feat(libSceNet): implement sceNetResolverStartNtoa6 Resolve a host name to its first IPv6 address through the host resolver (getaddrinfo with AF_INET6) and write the 16-byte address. The lookup shares the IPv4 path's resolver bookkeeping (abort preservation, error state); a name without an IPv6 address fails with RESOLVER_ENODNS like a failed lookup. Nonzero flags throw. Metal Gear Solid 4 Master Collection (PPSA18194) imports it and failed to load without it (unresolved zl35YNs9jnI from libSceNet.prx). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 36 +++++++++++++++++++++++++++--- core/libs/tests/GuestSceNet.cpp | 10 +++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7c34334844..27638ea00f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -14,6 +14,7 @@ #include #endif #include +#include #include #include #include @@ -1299,12 +1300,12 @@ int APS5_VABI sceNetResolverDestroy(int rid) { return g_resolvers.erase(rid) != 0 ? 0 : fail(NET_EBADF); } -int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { +int lookup_host(int rid, const char* hostname, const char* func, int family, std::vector>& addresses) { if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; addrinfo hints{}; - hints.ai_family = AF_INET; + hints.ai_family = family; addrinfo* results = nullptr; const int result = ::getaddrinfo(hostname, nullptr, &hints, &results); if (result != 0) { @@ -1313,13 +1314,31 @@ int lookup_ipv4(int rid, const char* hostname, const char* func, std::vectorai_next) { - const auto address = reinterpret_cast(entry->ai_addr)->sin_addr.s_addr; + if (entry->ai_family != family) continue; + std::array address{}; + if (family == AF_INET6) std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin6_addr, 16); + else std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin_addr, 4); if (std::find(addresses.begin(), addresses.end(), address) == addresses.end()) addresses.push_back(address); } ::freeaddrinfo(results); + if (addresses.empty()) { + set_errno(NET_ENOENT); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } return end_lookup(rid, generation, 0); } +int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { + std::vector> found; + if (const int error = lookup_host(rid, hostname, func, AF_INET, found)) return error; + for (const auto& address : found) { + std::uint32_t value = 0; + std::memcpy(&value, address.data(), sizeof(value)); + addresses.push_back(value); + } + return 0; +} + int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { (void)timeout; (void)retry; @@ -1331,6 +1350,17 @@ int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, return 0; } +int APS5_VABI sceNetResolverStartNtoa6(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartNtoa6: flags " + std::to_string(flags) + " are not supported"); + if (!hostname || !addr) return fail(NET_EINVAL); + std::vector> addresses; + if (const int error = lookup_host(rid, hostname, __func__, AF_INET6, addresses)) return error; + std::memcpy(addr, addresses.front().data(), addresses.front().size()); + return 0; +} + struct NetResolverRecord { std::uint32_t address; std::uint8_t address6[12]; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e88dc22a86..05c06bd611 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -52,6 +52,7 @@ extern const std::uint32_t sce_net_in6addr_any[4]; extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16]; int APS5_VABI sceNetResolverCreate(const char*, int, int); int APS5_VABI sceNetResolverStartNtoa(int, const char*, void*, int, int, int); +int APS5_VABI sceNetResolverStartNtoa6(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, int); @@ -837,6 +838,15 @@ int main() { resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(ipv4[0] == 127); + std::array resolved6{}; + resolved6.fill(0xA5); + Require(sceNetResolverStartNtoa6(resolver, "::1", resolved6.data(), 5000000, 1, 0) == 0); + Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array records{}; records.fill(0xA5); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323dc..95103ae7a7 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -11,3 +11,4 @@ - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does +- [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From 299a736b5422f40b9999ba370cba2125549ec53f Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Wed, 14 Oct 2026 00:44:56 +0530 Subject: [PATCH 167/353] feat(libSceCesCs): build libSceCesCs-module from the libSceCesCs source Titles that link the character conversion library under its module name need libSceCesCs-module.prx, which nothing built: Void Terrarium (PPSA03061, #5055) and Call of Duty: Black Ops (PPSA34501, #5389) were run with a copy of libSceCesCs.prx under that name. Both libraries are now built from the same source by add_sce_ces_library, as libScePsml and libSceFont-module are, and guest_ces_module runs the CesCs test against the module-named library. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs-module/CMakeLists.txt | 2 ++ core/libs/prx/libSceCesCs/CMakeLists.txt | 11 +++++++---- core/libs/prx/libSceCesCs/CesLibrary.cmake | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceCesCs-module/CMakeLists.txt create mode 100644 core/libs/prx/libSceCesCs/CesLibrary.cmake diff --git a/core/libs/prx/libSceCesCs-module/CMakeLists.txt b/core/libs/prx/libSceCesCs-module/CMakeLists.txt new file mode 100644 index 0000000000..64ae3a7a2e --- /dev/null +++ b/core/libs/prx/libSceCesCs-module/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceCesCs/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs-module) diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 82fbba577e..674db3453e 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -1,14 +1,17 @@ -add_library(libSceCesCs SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceCesCs PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceCesCs) +include(${CMAKE_CURRENT_SOURCE_DIR}/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs) if(BUILD_TESTING) add_test_executable(guest_ces_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) target_include_directories(guest_ces_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ces_tests PRIVATE libSceCesCs) add_test(NAME guest_ces COMMAND guest_ces_tests) + add_test_executable(guest_ces_module_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) + target_include_directories(guest_ces_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ces_module_tests PRIVATE libSceCesCs-module) + add_test(NAME guest_ces_module COMMAND guest_ces_module_tests) if(WIN32) - set_tests_properties(guest_ces PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces guest_ces_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/CesLibrary.cmake b/core/libs/prx/libSceCesCs/CesLibrary.cmake new file mode 100644 index 0000000000..dcad1c7bd6 --- /dev/null +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -0,0 +1,5 @@ +function(add_sce_ces_library target) + add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(${target}) +endfunction() From 808b2a83d3ad7f4ec724ee350203c49a0882c0f7 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 19:17:38 +0000 Subject: [PATCH 168/353] test(relinker): expect fixed executable ASLR policy --- core/relinker/relinker/tests/test_windows_fixed_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py index cd5d8cd5f2..85a0429cea 100644 --- a/core/relinker/relinker/tests/test_windows_fixed_base.py +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -70,7 +70,7 @@ def main(): assert bool(struct.unpack_from(" Date: Tue, 13 Oct 2026 11:24:24 +0200 Subject: [PATCH 169/353] feat(shader): link raw vertex prologs through user-data tails --- .../include/Driver/Draw/DrawCache.hpp | 1 + .../include/Driver/Shaders/ShaderRegistry.hpp | 4 +- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCache.cpp | 4 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 45 +++++- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 33 ++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 34 +++++ .../tests/execution/VertexUserDataTail.cpp | 142 ++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 2 + .../include/ControlFlow/GraphBuilder.hpp | 8 + .../ControlFlow/src/GraphBuilder.cpp | 28 ++++ docs/dev/debt/shader.md | 1 + 14 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 5fa2834621..77fc5ac4d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -23,6 +23,7 @@ struct DrawProgram { std::shared_ptr snapshot; std::size_t codeOffset = 0; + std::shared_ptr tailContinuation; }; struct DrawDecode { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 51334acc72..82c05c7ff9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -87,17 +87,19 @@ struct ShaderSnapshot { std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); std::shared_ptr> positionVectorUse = std::make_shared>(0u); + std::optional userDataTailRegister; }; std::shared_ptr ReadRawShader(std::uint64_t address); +std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); // The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program // without pos1..pos3 exports, so its artifact does not depend on them. -std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl); +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation = nullptr); bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); 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 e51868b6aa..418de2eef5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -145,7 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); - if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)) | graphics.layerExports; + if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 7844679fd2..f2ccc7140f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -120,7 +120,9 @@ void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry&) { return std::all_of(decode.programs.begin(), decode.programs.end(), [&](const DrawProgram& program) { - return !program.snapshot->header.empty() || ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot; + if (!program.snapshot->header.empty()) return true; + return ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot && + (program.tailContinuation == nullptr || ReadRawShader(program.tailContinuation->codeAddress) == program.tailContinuation); }); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 80b56997d2..101270bdbd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -51,6 +51,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i); result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i))); } + if (stage == Stage::Vertex && snapshot.header.empty() && snapshot.userDataTailRegister) result.tailContinuation = ReadUserDataTailContinuation(result.binary.code, result.firstUserSgpr, result.userData); return result; }; { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 8369224ac7..e7298e1f69 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -137,7 +137,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; const auto& y = b.programs[i]; - if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset) return false; + if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive) return false; return true; 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 4ad3671f97..9919737cf3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -91,6 +91,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t)); try { const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(snapshot.code); + if (const auto tail = ShaderRecompiler::FindUserDataTail(decoded, 0u, 106u)) snapshot.userDataTailRegister = tail->userDataRegister; const auto& last = decoded.instructions.back(); snapshot.code.resize(last.programCounter / sizeof(std::uint32_t) + last.wordCount); auto result = std::make_shared(std::move(snapshot)); @@ -120,6 +121,16 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { throw std::runtime_error("AGC driver: raw shader program has no reachable end within mapped code or the size limit"); } +std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData) { + const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + const auto tail = ShaderRecompiler::FindUserDataTail(program, userDataBase, static_cast(userData.size())); + if (!tail) return nullptr; + const auto slot = tail->userDataRegister - userDataBase; + const auto address = userData[slot] | (static_cast(userData[slot + 1u]) << 32u); + require(address != 0 && (address & 3u) == 0u, "user-data tail continuation address is null or not dword aligned"); + return ReadRawShader(address); +} + std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types) { if (auto registered = RegisteredProgram(shaders, address, types)) return registered; require(shaders.empty(), "graphics program does not belong to a compatible registered shader"); @@ -168,10 +179,11 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset return used; } -std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl) { +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation) { constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; if (bits == 0) return 0; + if (continuation != nullptr && PositionExportLayout(continuation, 0, continuation->code, bits) != 0) return bits; const bool cached = snapshot != nullptr && codeOffset == 0; if (cached) { const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); @@ -199,7 +211,22 @@ bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& regi const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second) << 40u); const auto snapshot = ProgramSnapshot(registry, address, {2}); require((address - snapshot->codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); - return (snapshot->header.empty() || snapshot->type == 2) && ProgramUsesWorkgroup(*snapshot, (address - snapshot->codeAddress) / sizeof(std::uint32_t)); + const auto offset = (address - snapshot->codeAddress) / sizeof(std::uint32_t); + if (!snapshot->header.empty()) return snapshot->type == 2 && ProgramUsesWorkgroup(*snapshot, offset); + if (!snapshot->userDataTailRegister) return ProgramUsesWorkgroup(*snapshot, offset); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8b); + const auto resources = queue.shader.find(0x8b); + const auto rsrc2 = resources == queue.shader.end() ? 0u : resources->second; + const auto count = ((rsrc2 >> 1u) & 0x1fu) | (((rsrc2 >> 27u) & 1u) << 5u); + require(count <= 32, "graphics user SGPR count exceeds the register bank"); + std::vector userData; + for (std::uint32_t index = 0; index < count; ++index) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8c + index); + const auto found = queue.shader.find(0x8c + index); + userData.push_back(found == queue.shader.end() ? 0u : found->second); + } + const auto continuation = ReadUserDataTailContinuation(std::span(snapshot->code).subspan(offset), 8u, userData); + return ProgramUsesWorkgroup(*snapshot, offset) || (continuation != nullptr && ProgramUsesWorkgroup(*continuation, 0u)); } struct ShaderPreparationTransaction::State { @@ -458,6 +485,15 @@ ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, con namespace { +std::vector LinkVertexUserDataTail(const ShaderRecompiler::RecompileRequest& request, const ShaderSnapshot& snapshot) { + if (request.shader.stage != ShaderRecompiler::ShaderStage::Vertex || !request.shader.header.empty() || !snapshot.userDataTailRegister) return {}; + const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(request.shader.code); + const auto tail = ShaderRecompiler::FindUserDataTail(program, request.context.userDataBaseRegister, static_cast(request.context.userData.size())); + if (!tail) return {}; + const auto continuation = ReadUserDataTailContinuation(request.shader.code, request.context.userDataBaseRegister, request.context.userData); + return ShaderRecompiler::LinkUserDataTail(program, *tail, ShaderRecompiler::RdnaInstructionDecoder{}.Decode(continuation->code)); +} + std::string hexText(std::uint64_t value) { char text[24]; std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); @@ -512,7 +548,8 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot"); const auto code = std::span(snapshot.code).subspan(codeOffset); require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code"); - const auto linked = LinkUserDataCallees(request); + auto linked = LinkUserDataCallees(request); + if (linked.empty()) linked = LinkVertexUserDataTail(request, snapshot); auto source = request; if (!linked.empty()) source.shader.code = linked; auto invocationRequest = source; @@ -970,7 +1007,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; std::optional vertex; if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); - if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl) | decoded.state.layerExports; + if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a78c75f9bc..46a79cf8de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -480,6 +480,38 @@ void verifyNullSwappc() { } +void VerifyUserDataTail() { + const RdnaInstructionDecoder decoder; + const std::vector code{0x7e000280u, 0xbe802008u, 0xbf9f0000u}; + const auto decoded = decoder.Decode(code); + const auto tail = FindUserDataTail(decoded, 8u, 2u); + require(tail && tail->programCounter == 4u && tail->userDataRegister == 8u, "unchanged user-data tail was not recognized"); + require(!FindUserDataTail(decoded, 9u, 2u) && !FindUserDataTail(decoded, 8u, 1u), "tail outside user data was accepted"); + const std::vector> refused{ + {0xbe880380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe890380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe880480u, 0xbe802008u, 0xbf9f0000u}, + {0xf4080200u, 0xfa000000u, 0xbe802008u, 0xbf9f0000u}, + {0xbf820000u, 0xbe802008u, 0xbf9f0000u}, + {0xbe802009u, 0xbf9f0000u}, + {0xbe802008u, 0xbf810000u}, + {0xbe802008u, 0xbf800000u, 0xbf9f0000u}, + }; + for (const auto& rejected : refused) require(!FindUserDataTail(decoder.Decode(rejected), 8u, 4u), "a modified, branching or malformed tail was accepted"); + const std::vector continuation{0xbf820001u, 0x7e000281u, 0xbf810000u}; + const auto linked = LinkUserDataTail(decoded, *tail, decoder.Decode(continuation)); + require(linked == std::vector({0x7e000280u, 0xbf820000u, 0xbf820001u, 0x7e000281u, 0xbf810000u}), "linking did not preserve relative branches and register operations"); + static_cast(GraphBuilder{}.Build(decoder.Decode(linked))); + const std::vector readsPc{0xbe8c1f00u, 0xbf810000u}; + bool refusedPc = false; + try { + static_cast(LinkUserDataTail(decoded, *tail, decoder.Decode(readsPc))); + } catch (const std::invalid_argument& error) { + refusedPc = std::string_view(error.what()).find("reads its program counter") != std::string_view::npos; + } + require(refusedPc, "a continuation reading its relocated PC was accepted"); +} + int main(int argc, char** argv) { try { verifyLoopMergeDoesNotEnterNestedSelection(); @@ -1279,6 +1311,7 @@ done: } try { verifyNullSwappc(); + VerifyUserDataTail(); verifyLargeLoopExitFallback(); verifyNestedExitForwarders(); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 21a2806af9..721edbaa26 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -658,6 +658,39 @@ void testProgramWorkgroupUse() { check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry"); } +void testVertexTailSnapshots() { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array prolog{0xbe802008u, 0xbf9f0000u}; + alignas(256) static std::array continuation{0xbf800000u, 0xbf810000u}; + const auto address = reinterpret_cast(prolog.data()); + const auto next = reinterpret_cast(continuation.data()); + const std::array data{static_cast(next), static_cast(next >> 32u)}; + const auto source = ReadRawShader(address); + check(source->userDataTailRegister == 8u, "raw tail metadata is missing"); + const auto target = ReadUserDataTailContinuation(source->code, 8u, data); + check(target && target->codeAddress == next, "tail did not read its user-data continuation"); + DrawDecode decode{}; + decode.programs.resize(1); + decode.programs[0].snapshot = source; + decode.programs[0].tailContinuation = target; + check(DecodeProgramsCurrent(decode, {}), "unchanged tail snapshot became stale"); + continuation[0] = 0xbf8a0000u; + check(!DecodeProgramsCurrent(decode, {}), "rewriting the continuation did not invalidate the draw cache"); + AgcDriver::QueueState queue{}; + queue.context[0x2d5] = 0x2000u; + queue.shader = {{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, + {0x8b, 4u}, {0x8c, data[0]}, {0x8d, data[1]}}; + check(VertexWorkgroupRequired(queue, {}), "a barrier in the continuation was hidden from workgroup routing"); + queue.context[0x2d5] |= 0x02000000u; + check(!VertexWorkgroupRequired(queue, {}), "a passthrough program was rerouted to a workgroup"); + const std::array nullAddress{}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, nullAddress)); }).find("address is null") != std::string::npos, "null tail address was accepted"); + const std::array unaligned{data[0] + 1u, data[1]}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unaligned)); }).find("dword aligned") != std::string::npos, "unaligned tail address was accepted"); + const std::array unmapped{0x1000u, 0u}; + check(!expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unmapped)); }).empty(), "unmapped tail address was accepted"); +} + void testRegisteredVertexWorkgroupRouting() { using namespace AgcDriver::DriverDetail; ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}}; @@ -781,6 +814,7 @@ int main(int argc, char** argv) { #endif testProgramSnapshots(); testProgramWorkgroupUse(); + testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); testEvents(); testValidation(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp new file mode 100644 index 0000000000..939a1722d3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -0,0 +1,142 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::byte Kept{0x40}; +using Image = std::array; +alignas(256) Image Pixels{}; +alignas(256) std::array Prolog{}; +alignas(256) std::array Reference{}; +alignas(256) std::array Continuation{0xbf840003u, 0x7e08020cu, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) const std::array OtherContinuation{0xbf840003u, 0x7e0802f4u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::uint32_t distanceLayout = 0) { + Pixels.fill(Kept); + const auto vertex = reinterpret_cast(code); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto next = reinterpret_cast(continuation); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, distanceLayout}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, + {0x8c, static_cast(next)}, {0x8d, static_cast(next >> 32u)}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "vertex tail draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +Image Check(std::span prefix, std::span continuation, std::uint32_t distanceLayout = 0, std::size_t continuationWords = 5) { + Prolog.fill(0xbf9f0000u); + Reference.fill(0xbf9f0000u); + std::copy(prefix.begin(), prefix.end(), Prolog.begin()); + Prolog[prefix.size()] = 0xbe802008u; + std::copy(prefix.begin(), prefix.end(), Reference.begin()); + std::copy_n(continuation.begin(), continuationWords, Reference.begin() + prefix.size()); + const auto expected = Draw(Reference.data(), continuation.data(), distanceLayout); + for (unsigned repeat = 0; repeat < 2u; ++repeat) { + const auto actual = Draw(Prolog.data(), continuation.data(), distanceLayout); + Require(actual == expected, "linked vertex tail pixels differ from the monolithic shader"); + } + return expected; +} + +void VertexTailDraws() { + std::vector prefix{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xbe8c03ffu, 0x3f800000u, 0xbf060c0cu, + }; + const auto original = Check(prefix, Continuation); + const auto kept = std::count(original.begin(), original.end(), Kept); + Require(kept != 0 && kept != static_cast(original.size()), "reference triangle did not cross the render target"); + const auto changedAddress = Check(prefix, OtherContinuation); + Require(changedAddress != original, "changing the continuation did not change the reference pixels"); + Continuation[1] = OtherContinuation[1]; + Require(Check(prefix, Continuation) == changedAddress, "rewritten continuation kept the old pixels"); + Continuation[1] = 0x7e08020cu; + Require(Check(prefix, Continuation) == original, "restored continuation did not restore the pixels"); + alignas(256) const std::array clipContinuation{0xbf840005u, 0x7e08020cu, 0xf80000cfu, 0x04030201u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; + const auto clipped = Check(prefix, clipContinuation, 0x00400001u, clipContinuation.size()); + Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); + const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); + Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + prefix.back() = 0xbf070c0cu; + const auto falseScc = Check(prefix, Continuation); + Require(std::ranges::all_of(falseScc, [](std::byte value) { return value == Kept; }), "SCC was not preserved across the tail jump"); + prefix.back() = 0xbf060c0cu; + prefix.push_back(0xbefe0480u); + const auto emptyExec = Check(prefix, Continuation); + Require(std::ranges::all_of(emptyExec, [](std::byte value) { return value == Kept; }), "EXEC was not preserved across the tail jump"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + VertexTailDraws(); + AgcDriverShutdown_nid_postfix(); + std::puts("vertex user-data tail 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/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index d4ff8cefaa..b3e04b8339 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -246,6 +246,8 @@ function(add_shader_recompiler target runtimeTarget) ShaderCacheDirectory SrgbDecodeBit SrgbUnormFormat + FindUserDataTail + LinkUserDataTail ThreadOwned UnresolvableSwappcTarget ValidateAndOptimizeSpirv diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index ada6f1084e..0b7cb0fb7d 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -10,6 +10,14 @@ namespace ShaderRecompiler { +struct UserDataTail { + std::uint32_t programCounter; + std::uint32_t userDataRegister; +}; + +[[nodiscard]] std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount); +[[nodiscard]] std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation); + struct SwappcInfo { bool fetchCallAllowed = false; std::uint32_t userDataBaseRegister = 0; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 3b623565eb..48bdd33cd0 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1011,6 +1011,34 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw return calls; } +std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount) { + if (program.instructions.size() < 2u || program.instructions.back().op != RdnaOpcode::SCodeEnd) return std::nullopt; + const auto& tail = program.instructions[program.instructions.size() - 2u]; + if (tail.op != RdnaOpcode::SSetpcB64 || tail.source0.kind != RdnaOperandKind::ScalarRegister) return std::nullopt; + const auto reg = tail.source0.reg; + if (reg % 2u != 0u || reg < userDataBase || userDataCount < 2u || reg - userDataBase >= userDataCount - 1u) return std::nullopt; + for (const auto& instruction : program.instructions) { + if (instruction.programCounter >= tail.programCounter) break; + if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64 || instruction.op == RdnaOpcode::SSwappcB64 || + instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd || + writesScalar(instruction, reg) || writesScalar(instruction, reg + 1u)) return std::nullopt; + } + return UserDataTail{tail.programCounter, reg}; +} + +std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation) { + const auto recognized = FindUserDataTail(program, tail.userDataRegister, 2u); + if (!recognized || recognized->programCounter != tail.programCounter) throw std::invalid_argument("user-data tail is not a linear jump through an unchanged register pair"); + for (const auto& instruction : continuation.instructions) { + if (instruction.op == RdnaOpcode::SGetpcB64) throw std::invalid_argument("user-data tail continuation reads its program counter, which a linked copy does not keep"); + } + const auto words = tail.programCounter / 4u; + std::vector linked(program.code.begin(), program.code.begin() + words); + linked.push_back(0xbf820000u); + linked.insert(linked.end(), continuation.code.begin(), continuation.code.end()); + return linked; +} + std::optional UserDataCalleeLength(std::span callee, std::uint32_t linkRegister) { std::uint32_t furthest = 0; for (std::uint32_t word = 0; word < callee.size();) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index de34303b91..5c91ef1580 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -18,6 +18,7 @@ ### Functional - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. +- [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Computed pointers, branching prologs, fragment epilogs and continuations reading `s_getpc_b64` remain unsupported - The [resource tracker](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans a [descriptor source](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) used only behind a conditional branch as inactive when the branch condition reads only clean (specialization) memory and so is evaluable at capture time; an inactive source is bound as zero words and not validated. Other conditions (EXEC, VCC, SCC set by vector or non-clean data, loop headers) leave every source active, as before, and a branch on a value that changes between dispatches is a specialization key like any other clean slot. PPSA15246 has a copy kernel that selects a buffer or an image path from an SRT word and reads the same address as a V# or a T#; before, the untaken path's descriptor was validated and threw. Whether the console would also accept an invalid descriptor in a path it does not take is assumed, not measured - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. From af33c8a1e54eb5f5695f6f7127fdcede7a02033c Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:43:35 +0200 Subject: [PATCH 170/353] feat(libSceAudioOut): implement sceAudioOut2DisableChat --- core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp | 5 +++++ core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 4 ++++ docs/dev/debt/libSceAudioOut.md | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 6ac66b3741..93afb2dabf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -38,4 +38,9 @@ int APS5_VABI sceAudioOut2EnableChat(void) { return 0; } +int APS5_VABI sceAudioOut2DisableChat(void) { + if (g_chatEnabled.exchange(false)) AUDIOOUT2_TRACE("t=%.3f DisableChat: chat audio is no longer allowed alongside the title's\n", AudioOut2TraceSeconds()); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 7482925465..b91b5d29df 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioOut2EnableChat(); +int APS5_VABI sceAudioOut2DisableChat(); } static void Require(bool value, const char* message) { @@ -64,6 +65,9 @@ void TestMasteringSetParam() { void TestEnableChat() { Require(sceAudioOut2EnableChat() == 0, "enabling chat must be accepted"); Require(sceAudioOut2EnableChat() == 0, "enabling chat again must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat again must be accepted"); + Require(sceAudioOut2EnableChat() == 0, "enabling chat after disabling it must be accepted"); } } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 7d7b5dfcc7..017fe07cf4 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -19,7 +19,7 @@ ### Functional -- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. `sceAudioOut2DisableChat` is not exported, as no title imports it. +- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. From 48b69fad4db3fec58bab959032f19286bad5eed7 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:49:19 +0200 Subject: [PATCH 171/353] feat(libc): implement sceLibcMspaceAlignedAlloc --- core/libs/prx/libc/src/Mspace.cpp | 4 ++++ core/libs/tests/Mspace.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index be9746f2b7..143d50eba2 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -261,6 +261,10 @@ void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void* handle, std::size_t alig return Allocate(Find(handle), size, alignment); } +void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t alignment, std::size_t size) { + return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); +} + void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) { if (alignment == 0 || (alignment & (alignment - 1))) { Error(22); diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index de030efb03..386435ded6 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -18,6 +18,7 @@ void APS5_VABI sceLibcMspaceFree_nid_postfix(void*, void*); int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t); std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void*); void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t); +void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::size_t); void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); @@ -187,6 +188,9 @@ int main() { void* memaligned = sceLibcMspaceMemalign_nid_postfix(arena, 256, 100); Require(memaligned && (reinterpret_cast(memaligned) & 255) == 0); Require(sceLibcMspaceMemalign_nid_postfix(arena, 24, 100) == nullptr); + void* alignedAlloc = sceLibcMspaceAlignedAlloc_nid_postfix(arena, 128, 256); + Require(alignedAlloc && (reinterpret_cast(alignedAlloc) & 127) == 0); + Require(sceLibcMspaceAlignedAlloc_nid_postfix(arena, 24, 100) == nullptr); unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); Require(realigned && (reinterpret_cast(realigned) & 63) == 0); for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); From d45fee9b69c8f59e11818d3e25fbdfc0b6e296fd Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:20:45 +0200 Subject: [PATCH 172/353] fix(import-audit): preserve JSON reports when writing fails --- tools/import_audit.py | 20 ++++- tools/tests/test_import_audit.py | 149 +++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index f0fd82f2d2..2cdf18e973 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -5,6 +5,7 @@ import re import struct import subprocess import sys +import tempfile from collections import defaultdict from pathlib import Path @@ -448,6 +449,23 @@ def attach_names(records, names): record["name_verified"] = nid_names.compute_nid(name) == record["nid"] +def write_json(path, result): + if path.is_dir(): + raise AuditError(f"{path}: JSON output is a directory") + destination = path.resolve() + with tempfile.TemporaryDirectory(dir=destination.parent, prefix=destination.name + ".", suffix=".tmp") as directory: + temporary = Path(directory) / destination.name + try: + temporary.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + if destination.exists(): + temporary.chmod(destination.stat().st_mode & 0o7777) + temporary.replace(destination) + finally: + if temporary.exists(): + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) + + def git_stamp(source=ROOT): try: commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip() @@ -544,7 +562,7 @@ def main(argv=None): "library": entry["stop"]["library"] if entry["stop"] else None, "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "stops": json_stops, "imports": records} - args.json.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + write_json(args.json, result) except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) return 2 diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 1e84d84ea6..d0e8c0f6e3 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -2,7 +2,9 @@ import contextlib import io import json import os +import stat import struct +import subprocess import sys import tempfile import unittest @@ -163,6 +165,153 @@ class ImportAuditTests(unittest.TestCase): self.work = Workspace() self.addCleanup(self.work.close) + def test_failed_json_write_preserves_existing_report_and_leaves_no_partial_report(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + + def disk_full(path, content, **kwargs): + with path.open("w", encoding="utf-8") as handle: + handle.write(content[:24]) + raise OSError(28, "No space left on device", str(path)) + + for exists in (True, False): + with self.subTest(existing_report=exists): + if exists: + output.write_bytes(previous) + else: + output.unlink(missing_ok=True) + files = set(work.root.iterdir()) + with patch.object(Path, "write_text", disk_full): + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("No space left on device", err) + self.assertEqual(set(work.root.iterdir()), files) + if exists: + self.assertEqual(output.read_bytes(), previous) + else: + self.assertFalse(output.exists()) + + def test_failed_json_replacement_preserves_report_and_cleans_staging(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + output.write_bytes(previous) + files = set(work.root.iterdir()) + with patch.object(Path, "replace", side_effect=PermissionError(13, "Permission denied", str(output))): + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("Permission denied", err) + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(set(work.root.iterdir()), files) + + def test_successful_json_write_replaces_complete_report_without_staging_files(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + output.write_text("old report", encoding="utf-8") + files = set(work.root.iterdir()) + code, _, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, err), (0, "")) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["implemented"], 1) + self.assertEqual(set(work.root.iterdir()), files) + + @unittest.skipUnless(sys.platform == "win32", "Windows read-only replacement behavior") + def test_read_only_json_report_stays_intact_without_a_read_only_temporary(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.root / "audit.json" + previous = b'{"previous": "complete report"}\n' + output.write_bytes(previous) + files = set(work.root.iterdir()) + output.chmod(0o444) + try: + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("FAIL:", err) + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(set(work.root.iterdir()), files) + finally: + output.chmod(0o666) + + def test_json_report_symlink_keeps_its_target(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + target = work.root / "actual-report.json" + target.write_text("old report", encoding="utf-8") + output = work.root / "audit.json" + try: + output.symlink_to(target.name) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + files = set(work.root.iterdir()) + code, _, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, err), (0, "")) + self.assertTrue(output.is_symlink()) + self.assertEqual(json.loads(target.read_text(encoding="utf-8"))["unique_imports"], 1) + self.assertEqual(set(work.root.iterdir()), files) + + def test_json_directory_error_preserves_the_supplied_path(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + output = work.modules / ".." / work.modules.name + self.assertNotEqual(str(output), str(output.resolve())) + files = set(work.root.iterdir()) + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertEqual(err, f"FAIL: {output}: JSON output is a directory\n") + self.assertEqual(set(work.root.iterdir()), files) + + @unittest.skipUnless(os.name == "posix", "POSIX report permissions") + def test_new_json_report_permissions_follow_the_process_umask(self): + script = "\n".join([ + "import os, sys", + "from pathlib import Path", + "sys.path.insert(0, sys.argv[1])", + "import import_audit", + "os.umask(int(sys.argv[4], 8))", + "Path(sys.argv[3]).write_text('{}\\n', encoding='utf-8')", + "import_audit.write_json(Path(sys.argv[2]), {'complete': True})", + ]) + for mask, mode in ((0o022, 0o644), (0o002, 0o664), (0o077, 0o600)): + with self.subTest(umask=oct(mask)): + output = self.work.root / f"audit-{mask:o}.json" + reference = self.work.root / f"plain-{mask:o}.json" + files = set(self.work.root.iterdir()) + completed = subprocess.run([sys.executable, "-c", script, str(import_audit.ROOT / "tools"), str(output), str(reference), oct(mask)], capture_output=True, text=True) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), stat.S_IMODE(reference.stat().st_mode)) + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) + self.assertEqual(set(self.work.root.iterdir()), files | {output, reference}) + + @unittest.skipUnless(os.name == "posix", "POSIX report permissions") + def test_existing_json_report_permissions_are_preserved(self): + output = self.work.root / "audit.json" + for mode in (0o600, 0o640, 0o664): + with self.subTest(mode=oct(mode)): + output.write_text("old report", encoding="utf-8") + output.chmod(mode) + files = set(self.work.root.iterdir()) + import_audit.write_json(output, {"complete": True}) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), mode) + self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {"complete": True}) + self.assertEqual(set(self.work.root.iterdir()), files) + def test_classification_accounts_for_every_reference(self): work = self.work (work.libs / "libSceMsgDialog.prx").write_bytes(build_elf([ From 742b7baba31407936f9bee2a71334a4b36d0d86a Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:49:00 +0200 Subject: [PATCH 173/353] feat(libSceNetCtl): implement sceNetCtlRegisterCallbackV6 and sceNetCtlUnregisterCallbackV6 --- core/libs/prx/libSceNetCtl/Export.cpp | 45 ++++++++++++++++++--------- core/libs/tests/GuestSceNet.cpp | 22 +++++++++++++ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 5f0ab11e26..c6645e18b1 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -34,6 +34,27 @@ static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; +static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; + +static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, 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; + *cid = index; + return 0; + } + } + return SCE_NET_CTL_ERROR_CALLBACK_MAX; +} + +static int UnregisterCallback(NetCtlCallback (&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; + return 0; +} static bool HostHasAddress(int family) { #ifdef _WIN32 @@ -169,26 +190,22 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(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 (!g_callbacks[index]) { - g_callbacks[index] = func; - *cid = index; - return 0; - } - } - return SCE_NET_CTL_ERROR_CALLBACK_MAX; + return RegisterCallback(g_callbacks, func, cid); +} + +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { + return RegisterCallback(g_callbacksV6, func, cid); } void APS5_VABI sceNetCtlTerm(void) { } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { - if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; - std::lock_guard lock(g_callbackLock); - g_callbacks[cid] = nullptr; - return 0; + return UnregisterCallback(g_callbacks, cid); +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { + return UnregisterCallback(g_callbacksV6, cid); } } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e88dc22a86..2006738cca 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -60,6 +60,8 @@ int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); @@ -983,6 +985,26 @@ int main() { Require(sceNetCtlGetIfStat(&second) == 0); 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)); + int cids[8] = {}; + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(cids[i] == i); + } + Require(sceNetCtlRegisterCallbackV6(dummyCallback, 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(cid == 3); + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlUnregisterCallbackV6(i) == 0); + } + } if (ipv6Unavailable) { std::puts("skipped the IPv6 socket checks, the host has no usable IPv6 loopback"); return 77; From 7092b01b0daab6afc52ce67d79f5d0f2537b7279 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:29:52 +0200 Subject: [PATCH 174/353] test(shader): preserve valid positions in vertex tail state checks --- .../tests/execution/VertexUserDataTail.cpp | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp index 939a1722d3..df9e6f2c6d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -27,20 +27,22 @@ alignas(256) std::array Reference{}; alignas(256) std::array Continuation{0xbf840003u, 0x7e08020cu, 0xf80008cfu, 0x04030201u, 0xbf810000u}; alignas(256) const std::array OtherContinuation{0xbf840003u, 0x7e0802f4u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array VertexColorPixelCode{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); commands.insert(commands.end(), payload); } -Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::uint32_t distanceLayout = 0) { +Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::uint32_t distanceLayout = 0, bool vertexColor = false) { Pixels.fill(Kept); const auto vertex = reinterpret_cast(code); - const auto pixel = reinterpret_cast(PixelCode.data()); + const auto pixel = reinterpret_cast(vertexColor ? VertexColorPixelCode.data() : PixelCode.data()); const auto next = reinterpret_cast(continuation); const std::vector> context{ {0x2d5, 0x2000}, - {0x1b6, 0}, {0x207, distanceLayout}, {0x200, 0}, {0x203, 0x800}, + {0x1b6, vertexColor ? 1u : 0u}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, + {0x207, distanceLayout}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, @@ -79,21 +81,50 @@ Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::ui return Pixels; } -Image Check(std::span prefix, std::span continuation, std::uint32_t distanceLayout = 0, std::size_t continuationWords = 5) { +Image Check(std::span prefix, std::span continuation, std::uint32_t distanceLayout = 0, std::size_t continuationWords = 5, bool vertexColor = false) { Prolog.fill(0xbf9f0000u); Reference.fill(0xbf9f0000u); std::copy(prefix.begin(), prefix.end(), Prolog.begin()); Prolog[prefix.size()] = 0xbe802008u; std::copy(prefix.begin(), prefix.end(), Reference.begin()); std::copy_n(continuation.begin(), continuationWords, Reference.begin() + prefix.size()); - const auto expected = Draw(Reference.data(), continuation.data(), distanceLayout); + const auto expected = Draw(Reference.data(), continuation.data(), distanceLayout, vertexColor); for (unsigned repeat = 0; repeat < 2u; ++repeat) { - const auto actual = Draw(Prolog.data(), continuation.data(), distanceLayout); + const auto actual = Draw(Prolog.data(), continuation.data(), distanceLayout, vertexColor); Require(actual == expected, "linked vertex tail pixels differ from the monolithic shader"); } return expected; } +void CheckBranchColors(const Image& written, const Image& skipped, const char* state) { + std::size_t covered = 0; + for (std::size_t pixel = 0; pixel < written.size(); pixel += 4) { + const bool untouched = written[pixel] == Kept; + for (std::size_t component = 0; component < 4; ++component) { + Require(written[pixel + component] == (untouched ? Kept : std::byte{0xff}) && + skipped[pixel + component] == (untouched ? Kept : std::byte{0}), std::string(state) + " did not select the expected vertex color"); + } + if (!untouched) ++covered; + } + Require(covered != 0 && covered != written.size() / 4, std::string(state) + " reference triangle did not cross the render target"); +} + +void ControlStateDraws(std::vector prefix) { + alignas(256) const std::array sccContinuation{0xbf840001u, 0x7e0c02f2u, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.insert(prefix.end() - 1, 0x7e0c0280u); + const auto trueScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + prefix.back() = 0xbf070c0cu; + const auto falseScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + CheckBranchColors(trueScc, falseScc, "SCC"); + alignas(256) const std::array execContinuation{0x7e0c02f2u, 0xbefe040eu, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.back() = 0xbf060c0cu; + prefix.push_back(0xbe8e047eu); + const auto activeExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + prefix.push_back(0xbefe0480u); + const auto emptyExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + CheckBranchColors(activeExec, emptyExec, "EXEC"); +} + void VertexTailDraws() { std::vector prefix{ 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, @@ -113,13 +144,7 @@ void VertexTailDraws() { Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); Require(culled == original, "a mixed-sign cull distance discarded the triangle"); - prefix.back() = 0xbf070c0cu; - const auto falseScc = Check(prefix, Continuation); - Require(std::ranges::all_of(falseScc, [](std::byte value) { return value == Kept; }), "SCC was not preserved across the tail jump"); - prefix.back() = 0xbf060c0cu; - prefix.push_back(0xbefe0480u); - const auto emptyExec = Check(prefix, Continuation); - Require(std::ranges::all_of(emptyExec, [](std::byte value) { return value == Kept; }), "EXEC was not preserved across the tail jump"); + ControlStateDraws(prefix); } } From 855823e85eb3765f6d0c1b171df0801407a4aa2c Mon Sep 17 00:00:00 2001 From: Jnbla6 Date: Mon, 12 Oct 2026 13:11:46 +0300 Subject: [PATCH 175/353] feat(libSceNgs2.native): implement custom sampler rack id 0x4001 (#4334) --- .../libSceNgs2.native/include/Ngs2Types.hpp | 16 ++++++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 7 ++- core/libs/prx/libSceNgs2.native/src/Rack.cpp | 9 +++- .../libs/prx/libSceNgs2.native/src/Render.cpp | 7 ++- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 24 ++++++--- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 51 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 33 ------------ docs/dev/debt/libSceNgs2.native.md | 1 + 8 files changed, 105 insertions(+), 43 deletions(-) delete mode 100644 docs/dev/TechnicalDebt.md diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index fd9cfd40ff..fd04539d34 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -19,6 +19,7 @@ static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_REVERB = 0x2001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_MASTERING = 0x3000; +static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; @@ -57,6 +58,10 @@ static constexpr std::uint32_t SCE_NGS2_REVERB_VOICE_PARAM_I3DL2 = 0x20010001; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP = 0x40010000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS = 0x40010001; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x40010004; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; @@ -275,6 +280,17 @@ struct Ngs2CustomRackOption { }; static_assert(sizeof(Ngs2CustomRackOption) == 1280); +struct Ngs2CustomSamplerRackOption { + Ngs2CustomRackOption custom_rack_option; + std::uint32_t max_channel_works; + std::uint32_t max_waveform_blocks; + std::uint32_t max_atrac9_decoders; + std::uint32_t max_atrac9_channel_works; + std::uint32_t max_ajm_atrac9_decoders; + std::uint32_t max_codec_caches; +}; +static_assert(sizeof(Ngs2CustomSamplerRackOption) == 1304); + struct Ngs2CustomSubmixerRackOption { Ngs2CustomRackOption custom_rack_option; std::uint32_t max_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index c77dc52054..5752cb73f8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -84,15 +84,20 @@ static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2Cust } void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { - if (voice.rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); SetupCustomSubmixer(voice, ParamAs(param)); return; } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); return; } + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 1acc40d6b6..a13a0508c9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -17,6 +17,7 @@ union RackOptions { Ngs2SubmixerRackOption submixer; Ngs2MasteringRackOption mastering; Ngs2ReverbRackOption reverb; + Ngs2CustomSamplerRackOption customSampler; Ngs2CustomSubmixerRackOption customSubmixer; }; @@ -26,13 +27,16 @@ static std::size_t RackOptionSize(std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); case SCE_NGS2_RACK_ID_REVERB: return sizeof(Ngs2ReverbRackOption); + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: return sizeof(Ngs2CustomSamplerRackOption); case SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER: return sizeof(Ngs2CustomSubmixerRackOption); default: throw std::runtime_error("NGS2: rack id " + Ngs2Hex(rackId) + " is not implemented"); } } static RackOptions DefaultRackOption(std::uint32_t rackId) { - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: default custom rack options are not implemented"); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER || rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::runtime_error("NGS2: default custom rack options are not implemented"); + } RackOptions options{}; auto& common = options.common; common.size = RackOptionSize(rackId); @@ -93,6 +97,7 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2CheckCustomRack(options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) { throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented"); @@ -115,6 +120,7 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->name[sizeof(rack->name) - 1] = '\0'; rack->maxGrainSamples = options.common.max_grain_samples; if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) rack->maxChannelWorks = options.customSampler.max_channel_works; if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) rack->maxInputs = options.customSubmixer.max_inputs; rack->bufferInfo = bufferInfo; @@ -125,6 +131,7 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO voice.ports.resize(options.common.max_ports); voice.matrices.resize(options.common.max_matrices); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2SetupUserFx(*rack, options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2SetupUserFx(*rack, options.customSubmixer.custom_rack_option); system->racks.push_back(rack); *handle = reinterpret_cast(rack); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 6cf69f7144..bf14563e82 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -242,8 +242,11 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { - if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); - else MixInputs(voice, voices, grain, systemRate); + if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + RenderSampler(voice, grain, systemRate); + } else { + MixInputs(voice, voices, grain, systemRate); + } if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index b6dfb53a3d..44e772c024 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -116,7 +116,10 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { return; } auto* dest = Ngs2FindVoice(patch.dest_handle); - if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) APS5_INVALID_ARG_EX; + if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || + dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + APS5_INVALID_ARG_EX; + } port.dest = dest; } @@ -281,14 +284,22 @@ static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (param.id) { - case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: SetupSampler(voice, ParamAs(param).format); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: AddWaveformBlocks(voice, ParamAs(param)); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP: + SetupSampler(voice, ParamAs(param).format); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + AddWaveformBlocks(voice, ParamAs(param)); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP: { const auto looping = std::find_if(voice.blocks.begin(), voice.blocks.end(), [](const Ngs2Block& block) { return block.info.num_repeats != 0; }); if (looping != voice.blocks.end()) looping->info.num_repeats = 0; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH: { const float ratio = ParamAs(param).ratio; if (!std::isfinite(ratio) || ratio < 0.0f) APS5_INVALID_ARG_EX; voice.pitch = ratio; @@ -317,7 +328,8 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { } if (rackId != voice.rack->rackId) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); switch (rackId) { - case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; + case SCE_NGS2_RACK_ID_SAMPLER: + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: ApplySamplerParam(voice, param); return; case SCE_NGS2_RACK_ID_SUBMIXER: if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) { const auto& fx = ParamAs(param); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index b5872f457e..8aa3ca28df 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -224,8 +224,59 @@ static void TestRackOptions() { Require(calls.cleanupVoices == (std::vector{0, 1})); } +static void TestCustomSampler() { + calls = {}; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomUserFx2ModuleOption modules[2] = {Module(Gain), Module(Offset)}; + + Ngs2CustomSamplerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 2; + for (std::uint32_t m = 0; m < 2; m++) { + option.custom_rack_option.module[m].option = &modules[m].custom_module_option; + option.custom_rack_option.module[m].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; + option.custom_rack_option.module[m].state_size = 16; + } + option.max_channel_works = 4; + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + Ngs2RackInfo rackInfo{}; + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_OK); + Require(rackInfo.rack_id == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackInfo.type == 4 && rackInfo.max_channel_works == 4); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::vector pcm(Grain * 2 * 4, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 1, 0, static_cast(pcm.size() / 2), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 1.0f}); + const Ngs2VoiceParamHeader exitLoop{sizeof(Ngs2VoiceParamHeader), 0, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP}; + Require(sceNgs2VoiceControl(voice, &exitLoop) == SCE_NGS2_OK); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + const auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); + TestCustomSampler(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md deleted file mode 100644 index 1a7c6bbe89..0000000000 --- a/docs/dev/TechnicalDebt.md +++ /dev/null @@ -1,33 +0,0 @@ -# Project technical debt - -Entries about one library or component are in [debt](debt/), one file per library or component, under the same sections as this page. Entries that belong to none stay here. - -### Build - -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. -- Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) -- Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - - libgcc_s_seh-1.dll - - libstdc++-6.dll - - libwinpthread-1.dll - -### Direct memory - -- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - -### Silent stubs - -Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: - -### Unknown function info - -- AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures -- `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` -- Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures -- The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters - -### Functional - -- [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. -- `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index ae86494224..1afbb6baca 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,3 +24,4 @@ - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented +- Custom sampler rack `0x4001` accepts built-in module `0x13` (Custom Voice Filter) and routes `0x40001300` Biquad parameters (identified via prosper logs and shadPS4 mapping), but throws for any filter type other than 1. \ No newline at end of file From ef52b16920b69cb212f2e5e21a6a5dd4ff65deb3 Mon Sep 17 00:00:00 2001 From: Jnbla6 Date: Mon, 12 Oct 2026 16:34:27 +0300 Subject: [PATCH 176/353] feat(libSceNgs2.native): add custom voice filter support and related tests for 0x13 issue case --- .../libSceNgs2.native/include/Ngs2Types.hpp | 29 +++++++++ .../libs/prx/libSceNgs2.native/src/Custom.cpp | 43 +++++++++++-- .../libSceNgs2.native/src/Ngs2Internal.hpp | 2 + core/libs/prx/libSceNgs2.native/src/Rack.cpp | 4 +- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 4 +- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 64 +++++++++++++++++++ 6 files changed, 138 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index fd04539d34..2e7886f412 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -63,10 +63,12 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x40010004; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; @@ -507,6 +509,33 @@ struct Ngs2CustomVoiceUserFx2Param { }; static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); +struct Ngs2CustomVoiceFilterParam { + Ngs2VoiceParamHeader header; + std::uint32_t type; + std::uint32_t channel_mask; + union { + struct { + float i0; + float i1; + float i2; + float o1; + float o2; + } direct; + struct { + float frequency; + float q; + float level; + std::uint32_t reserved; + std::uint32_t reserved2; + } fcq; + } param; + std::uint32_t reserved3; +}; +static_assert(sizeof(Ngs2CustomVoiceFilterParam) == 40); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, type) == 8); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, channel_mask) == 12); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, param) == 16); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 5752cb73f8..858f615a5a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -17,10 +17,16 @@ void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto& info = option.module[m]; - if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); - if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; - if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { + if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + } else { + throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); + } } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; @@ -34,10 +40,13 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, } void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.customModuleIds.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); for (std::uint32_t m = 0; m < option.num_modules; m++) { + rack.customModuleIds[m] = option.module[m].module_id; + if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; const auto& module = *reinterpret_cast(option.module[m].option); rack.userFx[m] = module; rack.userFxCommon[m].resize(module.common_size); @@ -58,6 +67,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { void Ngs2CleanupUserFx(Ngs2Rack& rack) { for (std::uint32_t i = 0; i < rack.voices.size(); i++) { for (std::size_t m = 0; m < rack.userFx.size(); m++) { + if (rack.customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; if (rack.userFx[m].cleanup_handler == nullptr) continue; auto context = UserFxContext(rack, m, i); const int result = rack.userFx[m].cleanup_handler(&context); @@ -90,6 +100,27 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { SetupCustomSubmixer(voice, ParamAs(param)); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } + const auto index = param.id & 0x1f; + if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_FILTER) APS5_INVALID_ARG_EX; + const auto& custom = ParamAs(param); + Ngs2SamplerVoiceFilterParam filter{}; + filter.header = custom.header; + filter.index = index; + filter.location = 1; + filter.type = custom.type; + filter.channel_mask = custom.channel_mask; + if (custom.type == 1) { + filter.frequency = custom.param.fcq.frequency; + filter.q = custom.param.fcq.q; + filter.level = custom.param.fcq.level; + } + Ngs2SetFilter(voice, filter); + return; + } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); @@ -102,10 +133,12 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { } void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { - if (voice.userFx.empty()) return; + if (voice.userFx.empty() || std::none_of(voice.rack->customModuleIds.begin(), voice.rack->customModuleIds.end(), + [](std::uint32_t id) { return id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; })) return; std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; for (std::size_t m = 0; m < voice.userFx.size(); m++) { + if (voice.rack->customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; auto& fx = voice.userFx[m]; const auto& module = voice.rack->userFx[m]; Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 7f78dc570d..8dbe429dc9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -156,6 +156,7 @@ struct Ngs2Rack { std::vector voices; std::vector userFx; std::vector> userFxCommon; + std::vector customModuleIds; }; struct Ngs2System { @@ -185,6 +186,7 @@ void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std:: int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo); int Ngs2ReleaseBuffer(const Ngs2BufferAllocator& allocator, Ngs2ContextBufferInfo bufferInfo, Ngs2ContextBufferInfo* outBufferInfo); void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index a13a0508c9..a9df1495ce 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -114,7 +114,9 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->system = system; rack->rackId = rackId; rack->maxChannels = RackMaxChannels(rackId, options); - rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0; + rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER ? options.customSampler.custom_rack_option.num_modules + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER ? options.customSubmixer.custom_rack_option.num_modules : 0; rack->uid = nextUid++; std::memcpy(rack->name, options.common.name, sizeof(rack->name)); rack->name[sizeof(rack->name) - 1] = '\0'; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 44e772c024..d90db41275 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -248,7 +248,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl } } -static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { if (param.index >= voice.rack->maxFilters) APS5_INVALID_ARG_EX; if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1); auto& filter = voice.filters[param.index]; @@ -305,7 +305,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para voice.pitch = ratio; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: SetFilter(voice, ParamAs(param)); break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs(param)); break; default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented"); } } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 8aa3ca28df..2b757b18a8 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -90,6 +90,17 @@ static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) { return false; } +template +static bool CreateSamplerThrows(const Ngs2CustomSamplerRackOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query); + } catch (const TException&) { + return true; + } + return false; +} + template static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) { param.header = {static_cast(sizeof(TParam)), 0, id}; @@ -274,9 +285,62 @@ static void TestCustomSampler() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestCustomFilter() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + + Ngs2CustomSamplerRackOption option{}; + option.custom_rack_option.rack_option.size = sizeof(option); + option.custom_rack_option.rack_option.max_grain_samples = 512; + option.custom_rack_option.rack_option.max_voices = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 1; + option.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + option.max_channel_works = 1; + + auto invalid = option; + invalid.custom_rack_option.module[0].state_size = 4; + Require(CreateSamplerThrows(invalid)); + invalid = option; + invalid.custom_rack_option.module[0].option = reinterpret_cast(1); + Require(CreateSamplerThrows(invalid)); + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const std::vector pcm(Grain, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + + Ngs2CustomVoiceFilterParam filter{}; + filter.type = 1; + filter.param.fcq.frequency = 10000.0f; + filter.param.fcq.q = 0.70710678f; + filter.param.fcq.level = 1.0f; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, filter); + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 1, filter)); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector out(Grain, -1.0f); + const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + Require(out[0] > 0.0f && out[0] < 0.1f && out.back() > out[0]); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); TestCustomSampler(); + TestCustomFilter(); return 0; } From 40ebfae70b6545fd5a28e631ec4beb2469768338 Mon Sep 17 00:00:00 2001 From: jnbla6 Date: Tue, 13 Oct 2026 20:45:41 +0300 Subject: [PATCH 177/353] fix: throw on unsupported filter types and update debt docs --- core/libs/prx/libSceNgs2.native/src/Custom.cpp | 2 ++ docs/dev/debt/libSceNgs2.native.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 858f615a5a..af850cd2b8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -117,6 +117,8 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { filter.frequency = custom.param.fcq.frequency; filter.q = custom.param.fcq.q; filter.level = custom.param.fcq.level; + } else { + throw std::runtime_error("NGS2: custom filter type " + std::to_string(custom.type) + " is not implemented"); } Ngs2SetFilter(voice, filter); return; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1afbb6baca..6cf33fa2fd 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,4 +24,4 @@ - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented -- Custom sampler rack `0x4001` accepts built-in module `0x13` (Custom Voice Filter) and routes `0x40001300` Biquad parameters (identified via prosper logs and shadPS4 mapping), but throws for any filter type other than 1. \ No newline at end of file +- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. \ No newline at end of file From 22f6b1735c04d82a2a9821bdbda5aadade3c858f Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 08:39:38 +1300 Subject: [PATCH 178/353] docs: keep the technical debt index and end the NGS2 entry with a newline The rebase onto main deleted docs/dev/TechnicalDebt.md, which is now the index that points into docs/dev/debt/. Restore it from main and add the missing final newline to docs/dev/debt/libSceNgs2.native.md. --- docs/dev/TechnicalDebt.md | 33 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 docs/dev/TechnicalDebt.md diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md new file mode 100644 index 0000000000..1a7c6bbe89 --- /dev/null +++ b/docs/dev/TechnicalDebt.md @@ -0,0 +1,33 @@ +# Project technical debt + +Entries about one library or component are in [debt](debt/), one file per library or component, under the same sections as this page. Entries that belong to none stay here. + +### Build + +- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. +- Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) +- Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): + - libgcc_s_seh-1.dll + - libstdc++-6.dll + - libwinpthread-1.dll + +### Direct memory + +- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. + +### Silent stubs + +Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: + +### Unknown function info + +- AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures +- `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` +- Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures +- The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters + +### Functional + +- [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. +- `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 6cf33fa2fd..1826329e43 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,4 +24,4 @@ - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented -- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. \ No newline at end of file +- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. From cf8c63b9b8720d62520333566d36ae2ba451370e Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:14:04 +0200 Subject: [PATCH 179/353] perf(libkernel): find a cached module's unwind tables without VirtualQuery on Windows Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Module/src/Module.cpp | 69 +++++++++++++------ core/libs/tests/GuestUnwindModuleInfo.cpp | 8 +++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 63a01ca89e..64d0d624ad 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -48,20 +48,30 @@ std::uint32_t ReadWord(std::uint64_t address) { return value; } -void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { +bool UnwindImageCurrent(const UnwindImage& image, const IMAGE_NT_HEADERS64& nt) { + return image.filled && image.size == nt.OptionalHeader.SizeOfImage && + (!image.hasTables || (ReadWord(image.tables.header) == image.headerWord && ReadWord(image.tables.frames) == image.framesWord)); +} + +void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleInfoForUnwind* info) { + if (image.hasTables) { + info->eh_frame_hdr_addr = image.tables.header; + info->eh_frame_addr = image.tables.frames; + info->eh_frame_size = image.tables.framesSize; + info->seg0_addr = reinterpret_cast(base); + info->seg0_size = image.size; + } + std::strncpy(info->name, image.name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} + +const UnwindImage& CurrentUnwindImage(void* allocationBase, const IMAGE_NT_HEADERS64& nt) { const auto* base = static_cast(allocationBase); - const auto* dos = reinterpret_cast(base); - if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); - std::lock_guard lock(unwindImagesMutex); auto& image = UnwindImages()[reinterpret_cast(base)]; - const bool current = image.filled && image.size == nt->OptionalHeader.SizeOfImage && - (!image.hasTables || (ReadWord(image.tables.header) == image.headerWord && ReadWord(image.tables.frames) == image.framesWord)); - if (!current) { + if (!UnwindImageCurrent(image, nt)) { UnwindImage fresh; - fresh.size = nt->OptionalHeader.SizeOfImage; - fresh.hasTables = EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); + fresh.size = nt.OptionalHeader.SizeOfImage; + fresh.hasTables = EhFrame::ReadEhmeta(reinterpret_cast(base), nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); if (fresh.hasTables) { fresh.headerWord = ReadWord(fresh.tables.header); fresh.framesWord = ReadWord(fresh.tables.frames); @@ -72,15 +82,33 @@ void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { fresh.filled = true; image = std::move(fresh); } - if (image.hasTables) { - info->eh_frame_hdr_addr = image.tables.header; - info->eh_frame_addr = image.tables.frames; - info->eh_frame_size = image.tables.framesSize; - info->seg0_addr = reinterpret_cast(base); - info->seg0_size = image.size; - } - std::strncpy(info->name, image.name.c_str(), sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; + return image; +} + +void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { + const auto* base = static_cast(allocationBase); + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); + std::lock_guard lock(unwindImagesMutex); + WriteUnwindInfo(CurrentUnwindImage(allocationBase, *nt), base, info); +} + +bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { + PVOID allocationBase = nullptr; + if (RtlPcToFileHeader(reinterpret_cast(address), &allocationBase) == nullptr || allocationBase == nullptr) return false; + const auto* base = static_cast(allocationBase); + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(reinterpret_cast(base)); + if (found == UnwindImages().end() || !found->second.hasTables || !UnwindImageCurrent(found->second, *nt)) return false; + info->st_size = sizeof(ModuleInfoForUnwind); + WriteUnwindInfo(found->second, base, info); + return true; } } @@ -155,6 +183,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn (void)flags; if (!info) return SCE_KERNEL_ERROR_EFAULT; #ifdef _WIN32 + if (FillKnownUnwindImage(addr, info)) return 0; MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) return SCE_KERNEL_ERROR_ESRCH; info->st_size = sizeof(ModuleInfoForUnwind); diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index bab2a0752f..f8479234b2 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -82,6 +82,11 @@ int main() { char mapped[4096] = {}; Require(GetMappedFileNameA(GetCurrentProcess(), memory.BaseAddress, mapped, sizeof(mapped) - 1) != 0); Require(std::strcmp(info.name, mapped) == 0); + for (const auto* inside : {static_cast(&fixture->header), static_cast(&fixture->frames)}) { + ModuleInfoForUnwind other{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(inside), 0, &other) == 0); + Require(std::memcmp(&other, &info, sizeof(info)) == 0); + } for (int repeat = 0; repeat < 3; ++repeat) { ModuleInfoForUnwind again{}; Require(sceKernelGetModuleInfoForUnwind(address, 0, &again) == 0); @@ -106,4 +111,7 @@ int main() { ModuleInfoForUnwind host{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &host) == 0); Require(host.eh_frame_hdr_addr == 0 && host.eh_frame_addr == 0 && host.eh_frame_size == 0); + ModuleInfoForUnwind hostAgain{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &hostAgain) == 0); + Require(std::memcmp(&hostAgain, &host, sizeof(host)) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index abf412acd5..bfc94bbe7b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,7 +30,7 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows -- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped when the loader frees the module (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release). A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. The `.eh_frame` walk that finds its end is done once per image instead of once per call +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped when the loader frees the module (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release). A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it From 3960519b1ef773defde72829bd227a531f1ca4f5 Mon Sep 17 00:00:00 2001 From: Zheorgyan Date: Tue, 13 Oct 2026 21:43:21 +0200 Subject: [PATCH 180/353] fix(agc): make host imports over writable pages on macOS Co-authored-by: Marcio Rocha Zacarias <6743187+mrzacarias@users.noreply.github.com> --- .../prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b284..059e3ed6ee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1212,7 +1212,14 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function saved.assign(source, source + (stop - first)); } const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); - if (!import()) return false; + bool imported = false; + { +#ifdef __APPLE__ + const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); +#endif + imported = import(); + } + if (!imported) return false; if (!staysWatched) { if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); unwatchLocked(tracker, address, bytes); From 852c808373eef6caf1a84046ed574f7b0a844a18 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 17:20:35 +0000 Subject: [PATCH 181/353] test(libSceNgs2): fix custom filter regression expectations --- core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 2b757b18a8..c810add593 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -293,6 +293,8 @@ static void TestCustomFilter() { option.custom_rack_option.rack_option.size = sizeof(option); option.custom_rack_option.rack_option.max_grain_samples = 512; option.custom_rack_option.rack_option.max_voices = 1; + option.custom_rack_option.rack_option.max_matrices = 1; + option.custom_rack_option.rack_option.max_ports = 1; option.custom_rack_option.num_buffers = 1; option.custom_rack_option.num_modules = 1; option.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; @@ -332,7 +334,7 @@ static void TestCustomFilter() { std::vector out(Grain, -1.0f); const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); - Require(out[0] > 0.0f && out[0] < 0.1f && out.back() > out[0]); + Require(out[0] > 0.0f && out[0] < 0.2f && out.back() > out[0]); Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } From 6adf63c7cc61e8c44fecee1979c92e4eae3319ea Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Wed, 14 Oct 2026 01:07:13 +0530 Subject: [PATCH 182/353] fix(relinker): share guest modules between executables that need different system libraries On Linux and macOS every guest module listed the converting executable's own DT_NEEDED system libraries, so its bytes could depend on which executable converted it. --executable then failed with "Guest modules convert differently" when the executables need different system libraries that the modules do not, or the same ones in another order. It is the error METAL GEAR SOLID: Peace Walker (PPSA18216, #5268) reports on Linux for its launcher and mgspw.elf. Guest modules now list only the system libraries the guest modules themselves need, with the libSceLibcInternal.prx to libc.prx rule. The executable's own DT_NEEDED is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../relinker/src/guest/GuestModuleBuilder.cpp | 8 +++++++- .../relinker/tests/test_executables.py | 18 ++++++++++++++++-- .../relinker/tests/test_guest_host_libc.py | 12 ++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index de2662f6ad..00631d142c 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -288,6 +288,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) 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()}); @@ -332,7 +338,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(), hostLibraries.begin(), hostLibraries.end()); + needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.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_executables.py b/core/relinker/relinker/tests/test_executables.py index b975342de7..baff6b86c1 100644 --- a/core/relinker/relinker/tests/test_executables.py +++ b/core/relinker/relinker/tests/test_executables.py @@ -4,6 +4,7 @@ import subprocess import sys import tempfile +from test_guest_host_libc import importing from test_guest_intel_trampolines import main_fixture from test_guest_module_directories import module_with_symbol, needed_libraries from test_guest_needed_modules import executable_with_needed @@ -14,13 +15,13 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-executables-") as directory: work = Path(directory) - def convert(case, windows, options, picker=None): + def convert(case, windows, options, picker=None, eboot=None): (case / "sce_module").mkdir(parents=True, exist_ok=True) (case / "sce_module" / "bundled.prx").write_bytes(module_with_symbol(True)) (case / "picker").mkdir(exist_ok=True) (case / "picker" / "picker.bin").write_bytes(main_fixture() if picker is None else picker) source = case / "eboot.bin" - source.write_bytes(main_fixture()) + source.write_bytes(main_fixture() if eboot is None else eboot) output = case / "out" / ("app.exe" if windows else "app.elf") output.parent.mkdir(exist_ok=True) result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", *options, str(source), str(output)], @@ -71,6 +72,19 @@ def main(): assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(secondary.read_bytes()) assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" not in needed_libraries(output.read_bytes()) + for label, windows, target in (("linux", False, []), ("macos", False, ["--macos"]), ("windows", True, [])): + for name, picker in (("subset", importing("libScePad.prx")), ("order", importing("libSceVideoOut.prx", "libScePad.prx"))): + case = work / f"{label}-host-{name}" + result, output = convert(case, windows, [*target, "--executable", "picker/picker.bin"], picker, importing("libScePad.prx", "libSceVideoOut.prx")) + assert result.returncode == 0, (label, name, result.stdout, result.stderr) + modules = sorted(output.parent.rglob("*.guest.prx")) + assert [path.name for path in modules] == ["bundled.prx.guest.prx"], (label, name, modules) + if label == "linux": + secondary = output.parent / "picker.picker.bin.elf" + assert needed_libraries(modules[0].read_bytes()) == [], needed_libraries(modules[0].read_bytes()) + assert "libSceVideoOut.prx" in needed_libraries(output.read_bytes()) + assert "libScePad.prx" in needed_libraries(secondary.read_bytes()) + if __name__ == "__main__": main() diff --git a/core/relinker/relinker/tests/test_guest_host_libc.py b/core/relinker/relinker/tests/test_guest_host_libc.py index bb1c776b05..7393dd72ec 100644 --- a/core/relinker/relinker/tests/test_guest_host_libc.py +++ b/core/relinker/relinker/tests/test_guest_host_libc.py @@ -6,6 +6,7 @@ import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture from test_guest_module_directories import needed_libraries +from test_guest_needed_modules import module_with_needed DYNAMIC = 0x4800 STRINGS = 0x4A00 @@ -60,6 +61,17 @@ def main(): needed = convert(relinker, work / "excluded", imports, ["libc.prx", "other.prx"], ["--exclude-sce-module", "libc.prx"]) assert needed == ["$ORIGIN/app0/sce_module/other.prx.guest.prx", *imports], needed + + case = work / "module-internal" + (case / "sce_module").mkdir(parents=True) + (case / "input.elf").write_bytes(importing("libkernel.prx")) + (case / "sce_module" / "user.prx").write_bytes(module_with_needed((b"libSceLibcInternal.prx",), (0,))) + result = subprocess.run([str(relinker), str(case / "input.elf"), str(case / "output.elf")], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + needed = needed_libraries((case / "output.elf").read_bytes()) + assert needed == ["$ORIGIN/app0/sce_module/user.prx.guest.prx", "libkernel.prx", "libSceLibcInternal.prx", "libc.prx"], needed + needed = needed_libraries((case / "app0" / "sce_module" / "user.prx.guest.prx").read_bytes()) + assert needed == ["libSceLibcInternal.prx", "libc.prx"], needed print("Guest host libc integration tests passed") From 8b0da182a0185e89c9cfd53841de44e7e2a61498 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 21:34:38 +0000 Subject: [PATCH 183/353] fix(ngs2): resolve voice patches to rack handles --- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 4 ++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 22 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index d90db41275..0b49d01cb3 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -116,6 +116,10 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { return; } auto* dest = Ngs2FindVoice(patch.dest_handle); + if (dest == nullptr) { + auto* rack = Ngs2FindRack(patch.dest_handle); + if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front(); + } if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index eac7d3fbd9..e9ae7428fa 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -147,6 +147,27 @@ static void TestPcmBlockEnd() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestPatchToRackHandle() { + const auto system = CreateSystem(); + const auto masteringRack = CreateRack(system, SCE_NGS2_RACK_ID_MASTERING); + const auto master = Voice(masteringRack); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 0, 0}); + Event(master, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Control(sampler, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 0, masteringRack}); + + Ngs2VoicePortInfo port{}; + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &port, sizeof(port)) == SCE_NGS2_OK); + Require(port.dest_handle == master && port.dest_input_id == 0); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto out = RenderI16(system); + Require(out == pcm); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static bool Near(float a, float b) { return std::fabs(a - b) < 1e-4f; } @@ -887,6 +908,7 @@ int main() { Require(std::atexit(RenderAfterStaticTeardown) == 0); TestErrorsAndInfo(); TestPcmBlockEnd(); + TestPatchToRackHandle(); TestPan(); TestPitchAndRepeat(); TestSubmixerMatrix(); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1826329e43..1eda352489 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -18,7 +18,7 @@ - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw -- [sceNgs2VoiceGetPortInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet +- [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason From eb78c6a45fee4c943d1984b878466dec77bdcc56 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 20:20:07 +0000 Subject: [PATCH 184/353] fix(libkernel): fail sceKernelStat with EFAULT for a null buffer Crimson Desert (PPSA25286) calls sceKernelStat with a null buffer at start-up, and main throws "sceKernelStat: sb is null", which ends the title in its first second. FreeBSD 11.4 looks the path up first and only then copies the result out, so a null buffer gets EFAULT for a path that exists and the lookup error for one that does not (sys/kern/vfs_syscalls.c, sys_stat): error = kern_statat(td, 0, AT_FDCWD, uap->path, UIO_USERSPACE, &sb, NULL); if (error == 0) error = copyout(&sb, uap->ub, sizeof (sb)); sceKernelStat now does the same: the lookup and its errors are as before, and a null buffer returns SCE_KERNEL_ERROR_EFAULT where the status would be copied. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 5 ++--- core/libs/tests/GuestFilesystem.cpp | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0868d227ca..53e270d8c8 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -291,15 +291,13 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (path == nullptr) { throw std::invalid_argument(std::string(__func__) + ": path is null"); } - if (sb == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": sb is null"); - } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); native = File::PathOrRelinkedModule(native); std::error_code error; constexpr int GuestEnotdir = 20; + constexpr int GuestEfault = 14; const auto status = std::filesystem::status(native, error); if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (!std::filesystem::exists(status)) { @@ -307,6 +305,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); File::FillFileStat(native, sb); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 987e09890d..7ce71eb6c9 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -210,6 +210,8 @@ int main() { Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2); Require(chmod_nid_postfix("anyps5_missing_chmod_path", 0600) == -1 && *__error_nid_postfix() == 2); Require(sceKernelChmod_nid_postfix("anyps5_missing_chmod_path", 0600) == static_cast(0x80020002u)); + Require(sceKernelStat("anyps5_missing_chmod_path", nullptr) == static_cast(0x80020002u)); + Require(sceKernelStat(".", nullptr) == static_cast(0x8002000eu)); Require(sceKernelDebugOutText(-1, "text") == static_cast(0x80020016u)); std::uint64_t throttling[4] = {1, 2, 3, 4}; Require(sceKernelWriteThrottlingStatus(throttling) == 0); From 6d33b36e7bccd1a6a4a714d98245030d2d8b6c4a Mon Sep 17 00:00:00 2001 From: ivica00 Date: Tue, 13 Oct 2026 22:26:53 +0200 Subject: [PATCH 185/353] fix(libc): enable indirect crash dumps for any nonempty environment value GetEnvironmentVariableA leaves the buffer undefined when the value does not fit, so a 16-byte read disabled longer settings. A size query treats every nonempty value as enabled and still leaves unset and empty disabled. Co-authored-by: Cursor --- core/libs/prx/libc/src/specifics/windows/CrashReport.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index f393d809b4..c64cd22ead 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -282,8 +282,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { Report(" r12 %016llx r13 %016llx r14 %016llx r15 %016llx\n", context->R12, context->R13, context->R14, context->R15); // The words each register points at, to recognise which structure a bad pointer came from. { - char indirectBuffer[16]{}; - const bool indirectMemory = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", indirectBuffer, sizeof(indirectBuffer)) > 0 && indirectBuffer[0] != '\0'; + const DWORD indirectLength = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); + const bool indirectMemory = indirectLength > 1; const std::pair pointers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; for (const auto& [name, value] : pointers) { if (value < 0x10000 || (value & 7) != 0 || !IsReadable(value) || !IsReadable(value + 0x38)) continue; From b6a10e91aa322e04fcff9564da98bc88bc0bb9b9 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 20:28:23 +0000 Subject: [PATCH 186/353] fix(libkernel): remove duplicate chflags error constant --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index f94bb212c6..2427c016f8 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -305,7 +305,6 @@ static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_ELOOP = 62; static constexpr int GUEST_EDQUOT = 69; static constexpr int GUEST_EOVERFLOW = 84; -static constexpr int GUEST_EOPNOTSUPP = 45; static int SceErrorFromErrno(int error); From 7b2104ee0d3a5b6d27aa7b3377bd2d137a3003da Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 09:36:10 +1300 Subject: [PATCH 187/353] fix(recompiler): accept SDWA source selects on v_cvt_f32_ubyte1, ubyte2 and ubyte3 v_cvt_f32_ubyte0 accepts every SDWA source selector, but the other three byte conversions had no VOP1 SDWA rule, so any SDWA source select on them was refused ("VOP1 SDWA source selector 4 of opcode 0x12 at pc 0xfcc is not supported"). The selected and optionally sign-extended source goes through the same operand path as for ubyte0 before byte N is extracted: SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads S0.u[8N+7:8N] (12.8). agc_driver_sdwa_cvt_f32_ubyte: WORD_0 and WORD_1 selects on ubyte1, and sign-extended WORD_1 and BYTE_3 selects on ubyte2 and ubyte3. --- .../tests/execution/SdwaCvtF32Ubyte.cpp | 104 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 3 + docs/dev/debt/shader.md | 1 + 3 files changed, 108 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp new file mode 100644 index 0000000000..4468d5d220 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp @@ -0,0 +1,104 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array SdwaCode{ + 0x34020082, 0x34060082, 0xe0302000, 0x80000401, + 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, + 0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xbf810000, +}; + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FloatBits(std::uint32_t value) { + return std::bit_cast(static_cast(value)); +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(SdwaCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto source = Input[tid * Inputs]; + const auto* out = &Output[tid * Results]; + const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; + const auto byte1 = FloatBits((source >> 8u) & 0xffu); + const auto byte3 = FloatBits(source >> 24u); + const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u); + Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8"); + Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24"); + Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word"); + Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device); + Check(); + std::puts("sdwa cvt f32 ubyte tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 1d9c25efe9..c0c29d52f8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1126,6 +1126,9 @@ constexpr Vop1SdwaRule vop1SdwaRules[] = { {RdnaOpcode::VCvtF32U32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32I32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32Ubyte0, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte1, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte2, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte3, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VNotB32, sdwaSelAll(), sdwaSelBytes() | sdwaSelWords(), sdwaSelAll(), false}, {RdnaOpcode::VFfblB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VFfbhU32, sdwaSelAll(), 0, 0, false}, diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index de34303b91..7163d55302 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -113,3 +113,4 @@ - [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. +- [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. From 28bcd6be5026d6883bf6e6b937d21e4e65cc44af Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Sun, 11 Oct 2026 21:11:01 +0200 Subject: [PATCH 188/353] fix(http2): allow completion waits without an event queue --- core/libs/prx/libSceHttp2/Export.cpp | 2 +- core/libs/tests/GuestHttp2.cpp | 29 +++++++++++++++++++++++++++- docs/dev/debt/libSceHttp2.md | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 786bf30997..36dfae81ec 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -75,7 +75,6 @@ static void EraseCompletions(const std::vector& ids) { } static int CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { - if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function); if (option != nullptr) NotImplemented_nid_no_patch(function); bool queued = false; { @@ -90,6 +89,7 @@ static int CompleteAsync(const char* function, int req_id, const Http2AsyncOptio } } if (!queued) return ERROR_INVALID_ID; + if (kqueue_option == nullptr) return 0; if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { { std::lock_guard lock(g_completionsMutex); diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index ffce322a38..d29c0021e4 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -154,7 +154,34 @@ int main() { Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); Require(result.req_id == request && result.result == static_cast(0x80436063)); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + std::uint32_t wait_timeout = 2000000; + Require(sceHttp2WaitAsync(request, &result, &wait_timeout, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + const int invalidRequests[] = {0, -1, 0x7fffffff, context, tmpl, box, doomedRequest, doomedTemplateRequest, doomedContextRequest}; + for (const int invalid : invalidRequests) { + Require(sceHttp2SendRequestAsync(invalid, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(invalid, &result, nullptr, nullptr); })); + } + const int independent = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/", 0); + Require(independent > 0 && independent != request); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, &tag); })); + Require(sceHttp2WaitAsync(independent, &result, nullptr, nullptr) == 0); + Require(result.req_id == independent && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); + for (int i = 0; i < 2; ++i) { + Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + } + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2DeleteRequest(independent) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); Require(sceKernelDeleteUserEvent(eq, request) == 0); Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); })); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index ec43ab5de9..3486ea1fd3 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -14,4 +14,4 @@ ### Functional -- [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw +- [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. PPSA15508 passes null kqueue and extra options at original VA 0xb63931, then calls `sceHttp2WaitAsync` with a 24-byte result and a 2,000,000 timeout, and checks the result at offset 8. A null kqueue option therefore retains the same offline completion for direct waiting without an event notification. The timeout is not used because the offline result is already complete. This path follows the original title call and caller control flow, not a console measurement. A supplied kqueue with a missing user event, an extra option, and a wait with no completed operation still throw From 2265e8b1cd3de88d2164703d47ec47427c19ffd0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 20:37:44 +0000 Subject: [PATCH 189/353] fix(shader): decode the high-half result of v_interp_p2_f16 Forspoken (PPSA03984) has a pixel shader with v_interp_p2_f16 v1, ..., v7 op_sel:[0,0,0,1], and main throws "VOP3 interpolation modifiers are not implemented" one second after start. On GFX10, op_sel[3] of a 16-bit VOP3 result selects the high half of the destination and keeps the other half. The decoder already maps that bit to destination.sdwaSel 5 for the other 16-bit VOP3 ops, and write16Bits/writeRawU32 then insert the result into bits 31:16 and keep bits 15:0. v_interp_p2_f16 is the only 16-bit-result interpolation instruction, so only it accepts the bit; other op_sel bits and the bit on the 32-bit-result p1ll/p1lv forms are still refused. agc_vop3_interp_decode checks the decoded selector with and without the bit, and that v_interp_p1ll_f16 still refuses it. In agc_driver_interpolation_f16 the second v_interp_p2_f16 now writes the high half of a register holding 0x00001234, and the GPU result must keep 0x1234 in the low half. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/Vop3InterpDecode.cpp | 17 +++++++++++++++++ .../tests/execution/InterpolationF16.cpp | 4 ++-- .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 4 +++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp index f362f67d6a..52027341ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -61,6 +61,7 @@ int main() { {{0xd6000005u, 0x02020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, {{0xd6000105u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, {{0xd6020005u, 0x22000200u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + {{0xd7424005u, 0x00020201u}, "modifiers are not implemented", "v_interp_p1ll_f16 with the high-half destination selector"}, }; int failures = 0; for (const auto& test : accepted) { @@ -104,5 +105,21 @@ int main() { ++failures; } } + for (const std::uint32_t selector : {0x0u, 0x4000u}) { + const std::vector code{0xd75a0005u | selector, 0x041e0201u, 0xbf810000u}; + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto& instruction = decoded.instructions.at(0); + const std::uint32_t half = selector != 0u ? 5u : 6u; + if (instruction.op != RdnaOpcode::VInterpP2F16 || instruction.destination.reg != Destination || instruction.destination.sdwaSel != half || + instruction.sourceCount != 4u || instruction.source0.reg != 1u || instruction.source1.value != 1u || instruction.source3.reg != 7u) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s decoded to the wrong opcode or operands\n", selector != 0u ? " op_sel:[0,0,0,1]" : ""); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s was refused: %s\n", selector != 0u ? " op_sel:[0,0,0,1]" : "", error.what()); + ++failures; + } + } return failures == 0 ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 8b519385a2..77dbe795c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -35,7 +35,7 @@ alignas(256) constexpr std::array VertexCode{ alignas(256) constexpr std::array PixelCode{ 0x7e080f02u, 0x7e0a0f03u, 0xd5430006u, 0x0411ff05u, 0x000000c0u, 0x7e300300u, 0x7e320301u, 0xd742001au, 0x00020000u, 0x7e3602ffu, 0xabcd0000u, 0xd75a001bu, 0x046a0200u, 0xd7420008u, 0x00020100u, 0x7e3802ffu, - 0x12340000u, 0xd75a001cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, + 0x00001234u, 0xd75a401cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, 0xd75a001eu, 0x04760340u, 0x7e3e0280u, 0xd75a801fu, 0x046a0200u, 0xd7420020u, 0x00020181u, 0xd7420021u, 0x000201c1u, 0xd7420022u, 0x40020000u, 0xd7430023u, 0x043e0000u, 0xe0782000u, 0x80011806u, 0xe0782010u, 0x80011c06u, 0xe0782020u, 0x80012006u, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, @@ -235,7 +235,7 @@ void Check(std::uint32_t waveSize) { result[1], std::bit_cast(lowX), 0xabcd0000u | HalfOf(SecondStep(Delta(LowX, 2), j, lowX)), - 0x12340000u | HalfOf(SecondStep(Delta(HighX, 2), j, highX)), + 0x1234u | (HalfOf(SecondStep(Delta(HighX, 2), j, highX)) << 16u), std::bit_cast(highY), HalfOf(SecondStep(Delta(HighY, 2), j, highY)), HalfOf(Saturate(SecondStep(Delta(LowX, 2), j, lowX))), diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 1d9c25efe9..18316a6787 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2090,7 +2090,8 @@ bool isVop3F16InterpolationOpcode(RdnaOpcode opcode) { void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { const bool hasSource2 = instruction.op != RdnaOpcode::VInterpP1llF16; const bool f16Result = instruction.op == RdnaOpcode::VInterpP2F16; - if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || opSel != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { + const std::uint32_t resultHalf = f16Result ? 0x8u : 0x0u; + if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || (opSel & ~resultHalf) != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); } if (src1 < 256u || (hasSource2 && src2 < 256u)) { @@ -2100,6 +2101,7 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst instruction.destination = DecodeRdnaVectorGpr(vdst); instruction.destination.clamp = clamp != 0u; instruction.destination.omod = omod; + if ((opSel & resultHalf) != 0u) instruction.destination.sdwaSel = 5u; instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); instruction.source0.absolute = (abs & 0x2u) != 0u; instruction.source0.negate = (neg & 0x2u) != 0u; From aec5ba133f40ae02a3925ea242c4587f80756a52 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 13 Oct 2026 17:47:28 -0300 Subject: [PATCH 190/353] fix(agc): report guest addresses above the user address space as unmapped on Windows A title can hand the GPU a buffer or descriptor V# based at an address above the Windows user-mode address space (Kena: Bridge of Spirits, PPSA01746, bases a V# at 0xf00000000000). VirtualQuery fails for any address above lpMaximumApplicationAddress, so describePages returned "not queried" and the driver aborted with "AGC driver: cannot query guest memory", although nothing can ever be mapped there and the shader never dereferences it. Above the user address limit, report the range as unmapped (as the Linux procmap/maps path already does for an uncovered range) instead of failing the query. The limit is read once from GetSystemInfo. Tested: Windows 11, MinGW-w64 GCC 15.2.0, Release, agc_driver_graphics_tests passes with the change and fails without it ("cannot query guest memory"). GPUs tested: NVIDIA GeForce RTX 4070 SUPER. --- .../libSceAgcDriver/Execution/src/GuestMemory.cpp | 13 +++++++++++++ core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b284..4188bfdd2c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -66,6 +66,15 @@ bool readableProtection(DWORD protection) { bool writableProtection(DWORD protection) { return protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY; } + +std::uintptr_t userAddressLimit() { + static const auto limit = [] { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return reinterpret_cast(info.lpMaximumApplicationAddress); + }(); + return limit; +} #endif } @@ -526,6 +535,10 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { // before it invalidates, so a generation change across the query and the store below means // the stored pages may describe the old state and are forgotten again. const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + if (cursor > userAddressLimit()) { + static_cast(emit(PageRun{cursor, end, false, false})); + return true; + } if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) return false; // Debug aid: APS5_TRACE_QUERY prints every 16th page query with its cost. static const bool traceQuery = std::getenv("APS5_TRACE_QUERY") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp index c7843ab418..b0dce8e47b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp @@ -39,6 +39,14 @@ void checkLiveStack(std::uintptr_t stackBottom, std::uintptr_t stackTop, std::ui void RunLiveStackAccessTests() { checkLiveStack(0, 0, 0); +#if defined(_WIN32) + for (const auto address : {std::uintptr_t{0xf00000000000ull}, std::uintptr_t{0x7fffffffffffull}}) { + Require(!GuestMemory::Accessible(reinterpret_cast(address), 4), + "an address above the user address space is reported accessible"); + const auto committed = GuestMemory::CommittedRanges(address, 4, false); + Require(committed.empty(), "an address above the user address space reports committed bytes"); + } +#endif #if defined(__linux__) const auto page = static_cast(sysconf(_SC_PAGESIZE)); const std::size_t stackBytes = 256 * 1024; From 1ba0a81820231598b2bdacf6f98ebd377b2cd3cb Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 16:24:09 +0200 Subject: [PATCH 191/353] feat(libkernel): implement getpriority for the guest process PS5MC (PPSA99420, Kodi 22) imports getpriority (miQ1wVwSexE) from libkernel.prx, one reference, next to chflags in its import order. Predicted stop once the imports ahead of it are covered (not a run): FAIL: unresolved ELF import miQ1wVwSexE from libkernel.prx The NID is computed with tools/nid_names.py and matches aerolib. Follows FreeBSD 11.4 sys/kern/kern_resource.c sys_getpriority (8c639945a44588f6cfdddfb057bb5aad88735da0) for the only guest process, taken to run at nice 0 and to lead its own process group (as kill and setsid already assume): PRIO_PROCESS and PRIO_PGRP with 0 or its pid, and PRIO_USER with 0 or its uid 0, return 0; other ids give ESRCH and an unknown which gives EINVAL. getpriority is a bare system call in FreeBSD's libc (lib/libc/sys/Symbol.map), so errno is left unchanged on success. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 63262245d274342b693fda146ae9feca3752b6e3) --- .../libs/prx/libkernel/System/src/Process.cpp | 19 ++++++++++++++ core/libs/tests/GuestSchedPriority.cpp | 26 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 46 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index b3d5ed436b..2f498dca06 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -118,10 +118,15 @@ bool hasPriorityRange(int policy) { return policy == 1 || policy == 3; } +constexpr int guestNoProcess = 3; constexpr int guestFault = 14; constexpr int guestInvalid = 22; constexpr int guestRlimitCpu = 0; constexpr int guestRlimitFsize = 1; +constexpr int guestPriorityProcess = 0; +constexpr int guestPriorityGroup = 1; +constexpr int guestPriorityUser = 2; +constexpr int guestNice = 0; constexpr int guestRlimitData = 2; constexpr int guestRlimitCore = 4; constexpr int guestRlimitSbsize = 9; @@ -548,6 +553,20 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { } } +int APS5_VABI getpriority_nid_postfix(int which, int who) { + if (which != guestPriorityProcess && which != guestPriorityGroup && which != guestPriorityUser) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + const bool found = which == guestPriorityUser ? (who == 0 ? getuid_nid_postfix() : who) == getuid_nid_postfix() + : who == 0 || who == getpid_nid_postfix(); + if (!found) { + *__error_nid_postfix() = guestNoProcess; + return -1; + } + return guestNice; +} + int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 0e9d418d11..8da97ce710 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -5,7 +5,13 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); int APS5_VABI sched_get_priority_min_nid_postfix(int policy); +int APS5_VABI getpriority_nid_postfix(int which, int who); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(); } +static constexpr int PrioProcess = 0; +static constexpr int PrioGroup = 1; +static constexpr int PrioUser = 2; static void Require(bool value) { if (!value) std::abort(); } static bool Rejects(int policy) { *__error_nid_postfix() = 0; @@ -13,6 +19,14 @@ static bool Rejects(int policy) { *__error_nid_postfix() = 0; return max && sched_get_priority_min_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; } +static void RequireNice(int which, int who) { + *__error_nid_postfix() = 13; + Require(getpriority_nid_postfix(which, who) == 0 && *__error_nid_postfix() == 13); +} +static void RequireError(int which, int who, int error) { + *__error_nid_postfix() = 0; + Require(getpriority_nid_postfix(which, who) == -1 && *__error_nid_postfix() == error); +} int main() { Require(Rejects(-1) && Rejects(2) && Rejects(5)); for (int policy : {1, 3}) { @@ -20,4 +34,16 @@ int main() { Require(sched_get_priority_min_nid_postfix(policy) == 256); } Require(Rejects(0) && Rejects(4)); + const int self = getpid_nid_postfix(); + RequireNice(PrioProcess, 0); + RequireNice(PrioProcess, self); + RequireNice(PrioGroup, 0); + RequireNice(PrioGroup, self); + RequireNice(PrioUser, 0); + RequireError(PrioProcess, self + 1, 3); + RequireError(PrioProcess, -1, 3); + RequireError(PrioGroup, self + 1, 3); + RequireError(PrioUser, 1000, 3); + RequireError(3, 0, 22); + RequireError(-1, self, 22); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d6991060b3..a1bc73db68 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -172,3 +172,4 @@ - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +- [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported From 091438f99ba64306cfa4f12248ecac624d4db1ba Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Tue, 13 Oct 2026 20:04:44 +0300 Subject: [PATCH 192/353] fix(libkernel): fail sysctl with ENOTDIR when a leaf MIB has a trailing component FreeBSD sysctl_find_oid returns ENOTDIR when the MIB continues below a leaf object. The supported hw.ncpu, hw.model, hw.physmem and kern.arandom MIBs threw an unsupported MIB error instead. (cherry picked from commit 6968aea8af578c4d56dc614cbe449274bb6e76e7) --- .../libkernel/System/src/SystemConfiguration.cpp | 8 ++++++++ core/libs/tests/GuestSystemConfiguration.cpp | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 672c9d2911..b558178345 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -44,6 +44,10 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol return 0; } +bool IsLeafMib(const int* name) { + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); +} + std::string HostCpuModel() { #if defined(__x86_64__) || defined(__i386__) if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; @@ -169,6 +173,10 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } + if (nameLength > 2 && IsLeafMib(name)) { + *__error_nid_postfix() = 20; + return -1; + } std::string mib; for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 1ad0f5c6f7..c599a5ac2f 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -156,6 +156,18 @@ static void CheckPathnameSysctl() { length = sizeof(buffer); Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } +static void CheckLeafWithTrailingComponent() { + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {1, 37}}; + for (const auto& leaf : leaves) { + const int mib[] = {leaf[0], leaf[1], 0}; + char buffer[8] = {}; + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); + Require(sysctl_nid_postfix(mib, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 20); + Require(length == sizeof(buffer)); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -257,6 +269,7 @@ int main() { #endif CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); + CheckLeafWithTrailingComponent(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); From f6aa98b29601498a4b8d356bb1e9f04a392b1d35 Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:36:27 +0100 Subject: [PATCH 193/353] feat(agc): render into SW_64KB_Z_X color targets SW_64KB_Z_X (0x18) color targets use the texture equations and 64 KiB block geometry of that swizzle mode, including the equation bits above the block that R_X targets already apply. PPSA12361 renders the R32 float copy of its depth into one. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/ColorTargetLayout.hpp | 5 +++-- .../Graphics/src/ColorTargetLayout.cpp | 11 ++++++----- .../prx/libSceAgcDriver/tests/ColorTargetLayout.cpp | 6 +++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ .../tests/execution/Uint8x4ColorTarget.cpp | 4 +++- docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index c586948030..f8acde495e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -18,12 +18,13 @@ enum class ColorTileMode : std::uint32_t { Standard64KB = 9, S4KBX = 0x15, D4KBX = 0x16, + Z64KBX = 0x18, D64KBX = 0x1a, RenderTarget = 0x1b }; constexpr bool ColorTileModeIsXor(ColorTileMode mode) { - return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; + return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::Z64KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; } constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { @@ -31,7 +32,7 @@ constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { } constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { - return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : TextureTileMode::kR64KBX; + return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : mode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX; } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 699325b2e5..6d1f9ec2fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -47,7 +47,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { require((attrib3 & 0x80002000u) == 0 && ((resourceType == 0 && (attrib3 & 0x1fffu) == 0) || resourceType == 1 || resourceType == 2) && ((attrib3 >> 27u) & 7u) == 1, "AGC graphics: unsupported color depth, dimension, resource level or metadata mode"); const auto mode = (attrib3 >> 14u) & 0x1fu; const auto fmaskMode = (attrib3 >> 19u) & 0x1fu; - require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); + require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x18 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); require(fmaskMode == 0 || fmaskMode == 0x18 || fmaskMode == (mode & ~3u), "AGC graphics: unsupported color FMASK swizzle mode"); require(resourceType != 0 || mode == 0 || mode == 0x1b, "AGC graphics: 1D color targets with a standard swizzle mode are invalid"); return static_cast(mode); @@ -74,9 +74,9 @@ struct SwizzleTables { }; const SwizzleTables& equationTables(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) { - static std::once_flag once[4][5]; - static SwizzleTables tables[4][5]; - const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : 3u; + static std::once_flag once[5][5]; + static SwizzleTables tables[5][5]; + const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : mode == ColorTileMode::Z64KBX ? 4u : 3u; const auto index = static_cast(std::countr_zero(bytesPerElement)); std::call_once(once[modeIndex][index], [&] { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); @@ -128,6 +128,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, } case ColorTileMode::S4KBX: case ColorTileMode::D4KBX: + case ColorTileMode::Z64KBX: case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: { const auto log2Elements = static_cast(std::countr_zero(ColorTileModeBlockBytes(mode))) - static_cast(std::countr_zero(bytesPerElement)); @@ -138,7 +139,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, const auto& tables = equationTables(mode, bytesPerElement, blockWidth, blockHeight); xOffsets = tables.x.data(); yOffsets = tables.y.data(); - if (mode == ColorTileMode::RenderTarget) equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); + if (mode == ColorTileMode::RenderTarget || mode == ColorTileMode::Z64KBX) equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); break; } case ColorTileMode::Standard4KB: diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 40786cc1e2..c77bdb157f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -210,6 +210,10 @@ void RunColorTargetLayoutTests() { } Require(DecodeColorTileMode(0x0dc68000u) == ColorTileMode::D64KBX, "the observed SW_64KB_D_X color descriptor was rejected"); + Require(DecodeColorTileMode(0x4dc60000u) == ColorTileMode::Z64KBX, "the observed SW_64KB_Z_X color descriptor was rejected"); + Require(ColorTextureTileMode(ColorTileMode::Z64KBX) == TextureTileMode::kZ64KBX, "a Z-order XOR color target did not map to its texture mode"); + Require(ColorTileModeIsXor(ColorTileMode::Z64KBX) && ColorTileModeBlockBytes(ColorTileMode::Z64KBX) == 65536u, "a Z-order XOR color target lost its block alignment or XOR flag"); + reject([] { DecodeColorTileMode((0x4dc60000u & ~(0x1fu << 19u)) | (1u << 19u)); }); Require(ColorTextureTileMode(ColorTileMode::D64KBX) == TextureTileMode::kD64KBX, "a display XOR color target did not map to its texture mode"); Require(ColorTileModeIsXor(ColorTileMode::D64KBX) && ColorTileModeBlockBytes(ColorTileMode::D64KBX) == 65536u, "a display XOR color target lost its block alignment or XOR flag"); reject([] { DecodeColorTileMode(0x0cc68000u); }); @@ -230,7 +234,7 @@ void RunColorTargetLayoutTests() { const auto block = ThinBlockLayout(TextureTileMode::kD64KBX, bpe); const auto width = 2u * block[1] + 1u; const auto height = 2u * block[2] + 1u; - for (const auto mode : {ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { + for (const auto mode : {ColorTileMode::Z64KBX, ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bpe); Require(equation != nullptr, "a 64 KiB color swizzle equation is missing"); const ColorTargetLayout layout(width, height, mode, bpe, 0x4600); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3092e67152..b545a2e2d3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -887,6 +887,15 @@ void ColorPipeBankXorTests() { const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR"); } + queue.context[0x3b8] = 0x4dc60000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xff00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Z64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_Z_X color base did not split its block address and XOR"); + } + queue.context[0x3b8] = 0x0dc68000u; for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) { queue.context[0x318] = static_cast((block + columnBits) >> 8u); queue.context[0x390] = static_cast((block + columnBits) >> 40u); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp index 8576fd86e0..dddb4482e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp @@ -51,7 +51,7 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 4> Programs{}; +alignas(256) std::array, 6> Programs{}; std::size_t programCount = 0; std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -246,6 +246,8 @@ int main() { Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x12u, 0xbeu, 0x42u, 0x7fu}, "a UINT16_ABGR export into an 8_8_8_8_UINT target"); Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x23u, 0x9au, 0x65u, 0x81u}, "a UINT16_ABGR export into a SW_64KB_D_X target", AgcDriver::Graphics::ColorTileMode::D64KBX); Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x31u, 0x7cu, 0x42u, 0x96u}, "a UINT16_ABGR export into a SW_64KB_D_X target with XOR", AgcDriver::Graphics::ColorTileMode::D64KBX, 0x4600); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x47u, 0x2bu, 0xd1u, 0x6eu}, "a UINT16_ABGR export into a SW_64KB_Z_X target", AgcDriver::Graphics::ColorTileMode::Z64KBX); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x5cu, 0x83u, 0x19u, 0xa4u}, "a UINT16_ABGR export into a SW_64KB_Z_X target with XOR", AgcDriver::Graphics::ColorTileMode::Z64KBX, 0x4600); std::puts("8_8_8_8_UINT color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a5d4bcb316..f79426e1ee 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -32,6 +32,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [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. - [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. From 696d1660c1e22971266827ecac41dbd8cf3ebf00 Mon Sep 17 00:00:00 2001 From: thibaut Date: Tue, 13 Oct 2026 09:36:55 +0200 Subject: [PATCH 194/353] feat(agc): storage image access to depth planes through 2D array views A 2D array storage view of a depth plane from slice 0 threw. It now gets a storage image over all its slices: each slice with a depth surface at the plane's address plus a whole depth slice per layer is copied from that surface before every use and back after the work; the other slices hold guest memory like any storage image. Cubes, stencil arrays, later base slices and layer pitches other than the depth slice size still throw. GTA V (PPSA04264) stops at compute shader 0x242d4a200 on a 5-slice R32 Z_64KB_X array over its shadow depth surfaces. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- .../Graphics/src/DepthSurface.cpp | 161 ++++++++++++++++-- .../tests/execution/DepthStorage.cpp | 4 +- .../tests/execution/DepthStorageDispatch.cpp | 78 ++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 227 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9337c6ef16..d3379ee102 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -153,19 +153,17 @@ public: float clearDepth = 0.0f; std::uint8_t clearStencil = 0; - std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { - if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr; + bool StorageViewMatches(std::span words, const GuestTextureResource& resource) const { + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr; - const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0; - if (resource.dimension != TextureDimension::k2D && !oneLayer) { - char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); - throw std::runtime_error(text); - } + return ResolveTextureFormat(resource.format) == expected || depthBits; + } + + void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const { + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented"); if (!stencil && hostFormat != target.format) { char text[224]; @@ -177,6 +175,18 @@ public: std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast(target.address)); throw std::runtime_error(text); } + } + + std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { + if (!StorageViewMatches(words, resource)) return nullptr; + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0; + if (resource.dimension != TextureDimension::k2D && !oneLayer) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); + throw std::runtime_error(text); + } + RequireStorable(resource, mip); ApplyFastClear(); auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) { @@ -439,6 +449,82 @@ std::map(bytes), nullptr, "storage image creation", PublishScope::None); + storage = StorageTexture::Create(context, *context.detiler, resource, 0); + Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers"); + staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + } + + void Copy(std::span slices, bool toStorage) { + if (slices.empty()) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + const auto commands = recorder->Commands(); + recorder->Keep(storage); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + std::vector storageRegions; + const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) { + VkBufferImageCopy copy{}; + copy.bufferOffset = sliceBytes() * layer; + copy.imageSubresource = {aspect, 0, imageLayer, 1}; + copy.imageExtent = {extent.width, extent.height, 1}; + return copy; + }; + for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer)); + const auto copyToBuffer = context.Function("vkCmdCopyImageToBuffer"); + const auto copyToImage = context.Function("vkCmdCopyBufferToImage"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + if (toStorage) { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©); + } + } else { + copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast(storageRegions.size()), storageRegions.data()); + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + if (toStorage) { + copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast(storageRegions.size()), storageRegions.data()); + } else { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } + } + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + recorder->EndGpuTiming(timing, sliceBytes() * slices.size()); + } + + const Context context; + const DepthTarget base; + const VkExtent2D extent; + const std::uint32_t bytesPerTexel; + const std::uint32_t layers; + std::shared_ptr storage; + +private: + std::unique_ptr staging; + + VkDeviceSize sliceBytes() const { + return static_cast(extent.width) * extent.height * bytesPerTexel; + } +}; + +std::map, std::unique_ptr>& layeredStorages() { + static auto* storages = new std::map, std::unique_ptr>(); + return *storages; +} + bool sameSurface(const DepthTarget& a, const DepthTarget& b) { return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples; } @@ -575,6 +661,7 @@ void ClearDepthSurfaces(VkDevice device) { drawnDepth = nullptr; std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); + std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); } std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { @@ -682,17 +769,71 @@ bool DepthSurfaceAt(const Context& context, std::span words return ResolveTextureFormat(resource.format) == expected || depthBits; } +namespace { + +std::vector layeredSlices(const Context& context, const LayeredDepthStorage& storage) { + const auto& base = storage.base; + std::vector slices; + const auto& list = surfaces(); + for (std::uint32_t layer = 0; layer < storage.layers; ++layer) { + const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); + if (slice != list.rend()) slices.push_back({slice->get(), layer}); + } + return slices; +} + +std::shared_ptr layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) { + const auto& target = surface.target; + const auto layers = resource.depthOrLastArray + 1u; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytesPerTexel = d16 ? 2u : 4u; + if (resource.baseArray != 0) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast(target.address), resource.baseArray, resource.depthOrLastArray); + throw std::runtime_error(text); + } + surface.RequireStorable(resource, mip); + const auto geometry = DescribeSurface(resource); + if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast(target.address), static_cast(geometry.GuestLayerOffset(1)), static_cast(DepthSliceBytes(target.extent, bytesPerTexel))); + throw std::runtime_error(text); + } + auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension)}]; + if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, layers); + else static_cast(entry->storage->Refresh()); + const auto slices = layeredSlices(context, *entry); + for (const auto& slice : slices) slice.surface->ApplyFastClear(); + entry->Copy(slices, true); + return entry->storage; +} + +} + std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); - return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip); + if (found == list.rend()) return nullptr; + auto& surface = **found; + const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress; + if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip); + if (!surface.StorageViewMatches(words, resource)) return nullptr; + return layeredDepthStorage(context, surface, resource, mip); } bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) { std::lock_guard lock(surfacesMutex()); + for (auto& [key, entry] : layeredStorages()) { + if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; + const auto slices = layeredSlices(context, *entry); + entry->Copy(slices, false); + for (const auto& slice : slices) slice.surface->written = ++depthWrites; + return true; + } for (const auto& surface : surfaces()) { if (surface->context.device == context.device && surface->StoreBack(storage)) return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 2c0031e06c..ec58a19d3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -243,10 +243,10 @@ void Run(Context context, std::uint8_t* block) { layeredStencil.depthOrLastArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); layeredDepth.depthOrLastArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the depth plane was accepted"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); layeredDepth.depthOrLastArray = 0; layeredDepth.baseArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a 2D array view of layer 1 of the depth plane was accepted"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "through a 2D array view of slices 1-0", "a 2D array view of layer 1 of the depth plane was accepted"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp index 9b8dba980d..cf2517f292 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -39,7 +39,8 @@ constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t TileLinear = 0; constexpr std::uint32_t TileStandard256B = 1; -constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t TileZ64KBX = 0x18; +constexpr std::size_t BlockBytes = 2u << 20u; constexpr std::size_t StencilOffset = 1u << 16u; constexpr std::size_t OutputOffset = 2u << 16u; constexpr std::size_t MultisampledOffset = 4u << 16u; @@ -48,6 +49,8 @@ constexpr std::size_t SampledOffset = 8u << 16u; constexpr std::size_t LaterDepthOffset = 10u << 16u; constexpr std::size_t ArrayDepthOffset = 12u << 16u; constexpr std::size_t ArrayStencilOffset = 13u << 16u; +constexpr std::size_t ArrayOffset = 16u << 16u; +constexpr std::uint32_t ArrayLayers = 3; alignas(256) constexpr std::array ScaleDepthCode{ 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, @@ -247,13 +250,77 @@ void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std: RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the one-layer 2D array view's depth"); RunPlane(device, layer(AddStencilLayerCode, stencil, Format8Uint, 0.0f), AddStencilLayerCode, output, 5u, "stencil dispatch through a one-layer 2D array view"); RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the one-layer 2D array view's stencil"); +} +ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* layers, std::uint32_t layer) { std::vector userData(16, 0u); - const auto texture = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(layers, Format32Float, Type2DArray, TileZ64KBX, ArrayLayers - 1u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - userData[14] = std::bit_cast(1.0f / 64.0f); - const auto program = Compile(device, StoreLayerCode, userData); - ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); }, "storage image access to depth/stencil surface", "a two-layer 2D array storage view of the depth plane"); + userData[12] = layer; + userData[14] = std::bit_cast(1.0f / 128.0f); + return Compile(device, code, userData); +} + +void StoreLayer(AgcDriver::VulkanDevice& device, std::span layers, std::span output, std::uint32_t layer) { + const auto store = LayerProgram(device, StoreLayerCode, output, layers.data(), layer); + device.Dispatch(store, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); + device.WaitIdle(); +} + +void RequireGuestLayer(std::span layer, bool stored, float rest, float initial, const std::string& what) { + std::vector seen(Threads, 0u); + std::uint32_t kept = 0; + for (std::size_t offset = 0; offset < layer.size(); offset += sizeof(float)) { + float texel = 0.0f; + std::memcpy(&texel, layer.data() + offset, sizeof(texel)); + const auto lane = static_cast(texel * 128.0f); + if (stored && texel < 0.25f && texel != initial && static_cast(lane) / 128.0f == texel) { + ++seen[lane]; + continue; + } + if (texel == rest) { + ++kept; + continue; + } + Require(texel == initial, what + ": guest texel at offset " + std::to_string(offset) + " is " + std::to_string(texel) + ", expected " + std::to_string(rest) + " or the untouched " + std::to_string(initial)); + } + const auto texels = static_cast(layer.size() / sizeof(float)); + const auto expected = (rest == initial ? texels : Width * Height) - (stored ? Threads : 0u); + Require(kept == expected, what + ": " + std::to_string(kept) + " guest texels hold " + std::to_string(rest) + ", expected " + std::to_string(expected)); + if (!stored) return; + for (std::uint32_t lane = 0; lane < Threads; ++lane) Require(seen[lane] == 1u, what + ": the value stored by lane " + std::to_string(lane) + " is in guest memory " + std::to_string(seen[lane]) + " times"); +} + +void LayeredArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + const auto sliceBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(sliceBytes == 1u << 16u, "a depth slice of the test extent is not one 64 KiB block"); + const std::span layers(block + ArrayOffset, sliceBytes * ArrayLayers); + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const auto guestLayer = [&](std::uint32_t layer) { return layers.subspan(sliceBytes * layer, sliceBytes); }; + const float untouched = 0.875f; + const float guestDepth = 0.75f; + for (std::uint32_t layer = 0; layer < ArrayLayers; ++layer) { + const float value = layer == 2 ? guestDepth : untouched; + for (std::size_t offset = 0; offset < sliceBytes; offset += sizeof(float)) std::memcpy(guestLayer(layer).data() + offset, &value, sizeof(float)); + } + const Surface first{reinterpret_cast(guestLayer(0).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface second{reinterpret_cast(guestLayer(1).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, first, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the first layer's depth producer did not cover its surface"); + Require(Draw(device, shaders, second, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the second layer's depth producer did not cover its surface"); + + StoreLayer(device, layers, output, 1); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the values a 2D array storage view stored to the second layer's surface"); + Require(Draw(device, shaders, first, {.depthCompare = VK_COMPARE_OP_GREATER}) == 0, "a store to the second layer changed the first layer's surface"); + StoreLayer(device, layers, output, 2); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second layer's surface after a store to the third layer"); + + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(layers.data()), layers.size(), nullptr, "test"); + device.WaitIdle(); + RequireGuestLayer(guestLayer(0), false, 0.5f, untouched, "the first layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(1), true, 0.5f, untouched, "the second layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(2), true, guestDepth, guestDepth, "the third layer in guest memory after the flush"); } void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) { @@ -334,6 +401,7 @@ int main() { const auto shaders = DepthFastClearHarness::Compile(*device); StoreBackCase(*device, shaders, block.Data()); ArrayViewCase(*device, shaders, block.Data()); + LayeredArrayViewCase(*device, shaders, block.Data()); MultisampledCase(*device, shaders, block.Data()); TwoViewsCase(*device, block.Data()); SampledCopyCase(*device, shaders, block.Data()); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a5d4bcb316..b95da0d537 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -29,7 +29,7 @@ - [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. -- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. +- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, or a sampled copy) is a separate image, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. From 9be93156be6d24ceccbd6a2bb27c85c5eb7191ff Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 11:13:55 +0100 Subject: [PATCH 195/353] feat(libkernel): add getdtablesize VLC (PPSA85300) fails to load on the missing getdtablesize import now that readdir_r is in. Its only call is in a spawn child that never runs, and the console's descriptor limit is unknown, so the export throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Process.cpp | 5 +++++ core/libs/tests/GuestGetrlimit.cpp | 8 ++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 14 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 2f498dca06..792d852ddb 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -567,6 +567,11 @@ int APS5_VABI getpriority_nid_postfix(int which, int who) { return guestNice; } +int APS5_VABI getdtablesize_nid_postfix(void) { + NotImplemented_nid_no_patch("getdtablesize: the console's descriptor limit"); + return -1; +} + int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index f597a80211..5312d114f1 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -15,6 +15,7 @@ struct ResourceLimit { extern "C" int APS5_VABI getrlimit_nid_postfix(int resource, ResourceLimit* limit); extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getdtablesize_nid_postfix(void); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; @@ -124,4 +125,11 @@ int main() { }) { RequireUnsupported(resource); } + bool unsupported = false; + try { + getdtablesize_nid_postfix(); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a1bc73db68..950ef652b6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -90,6 +90,7 @@ - [sched_get_priority_max, sched_get_priority_min](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` and `scePthreadSetschedparam` accept those two policies with a priority of 256 to 767 or 896 to 959 and keep the policy, as measured; a created thread takes the policy of explicit scheduling attributes, or its creator's when it inherits. When it inherits, it still takes the priority of its attributes, where the console gives it its creator's priority - [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result +- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` ### Functional From bfe78af9f57e062ee82a96265ee6c91cf9bca164 Mon Sep 17 00:00:00 2001 From: ps54all Date: Tue, 13 Oct 2026 23:56:39 +0200 Subject: [PATCH 196/353] feat(libSceAppContent): export sceAppContentAddcontDelete No additional content is ever installed, so deleting one finds nothing: a null entitlement label answers PARAMETER and any other NOT_FOUND, the same way sceAppContentAddcontEnqueueDownload does. Metal Gear Solid 4 Master Collection (PPSA18194) imports it in its game executable, which fails to load without it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAppContent/Export.cpp | 6 ++++++ core/libs/tests/GuestAppContent.cpp | 3 +++ docs/dev/debt/libSceAppContent.md | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 5f56318a14..c3b44b3d75 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -178,6 +178,12 @@ int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const return SCE_APP_CONTENT_ERROR_NOT_FOUND; } +int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + (void)service_label; + if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} + int APS5_VABI sceAppContentGetAddcontDownloadProgress(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 1f68eaec06..8fb8a16677 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBoot int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -85,6 +86,8 @@ int main() { Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); Require(sceAppContentAddcontEnqueueDownload(0, &label) == ErrorNotFound); Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontDelete(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontDelete(7, nullptr) == ErrorParameter); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == ErrorParameter); Require(sceAppContentAddcontMount(0, &label, &mountPoint) == ErrorNotFound); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1c9129423e..957dc51912 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -8,5 +8,5 @@ ### Functional -- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified +- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From 8aa0d428242030fa62128e432bdbebaf986d1b21 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 14:18:58 +0200 Subject: [PATCH 197/353] feat(libc): implement swscanf swscanf (HNnWdT43ues) runs the swscanf_s scanner without the buffer size arguments after %s, %c and %[, as FreeBSD's swscanf.c passes its arguments to vswscanf. Both entry points share one engine with a flag for the sizes; swscanf_s behaves as before. Title: PS5CEMU-HAR PPSA99360 imports swscanf from libSceLibcInternal.prx (import audit with --trap-sdk-syscall-stubs, registry index 292, right after pthread_setaffinity_np at 276). Predicted stop line once pthread_setaffinity_np resolves, without this change: FAIL: unresolved ELF import HNnWdT43ues (swscanf) from libSceLibcInternal.prx NID from tools/nid_names.py, confirmed in shadPS4 aerolib.inl:51950 (e0a466753a1a). Prototype in ps5-payload-dev include/freebsd/wchar.h:133, export in sce_stubs/libSceLibcInternal.c:8582 (f5d40f02770e). Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/ScanningWide.cpp | 95 ++++++++++++++----------- core/libs/tests/GuestWideConversion.cpp | 58 +++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 113 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 04917fdeba..e03bf6a2d4 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -13,6 +13,14 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsFormatting.hpp" +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + extern "C" int APS5_VABI iswspace_nid_postfix(std::uint32_t c); extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); @@ -122,15 +130,9 @@ Fit FloatFit(std::string_view text) { return Fit::Valid; } -std::size_t IntegerSize(const std::string& length) { - if (length.empty()) return sizeof(std::int32_t); - if (length == "hh") return sizeof(std::int8_t); - if (length == "h") return sizeof(std::int16_t); - if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); - throw std::invalid_argument("swscanf_s: invalid integer length modifier " + length); -} - class WideScanner { + const char* const name; + const bool sized; const char16_t* const start; const char16_t* input; const char16_t* format; @@ -139,8 +141,8 @@ class WideScanner { bool converted = false; public: - WideScanner(const char16_t* buffer, const char16_t* formatText, const void* source) - : start(buffer), input(buffer), format(formatText), args(source) {} + WideScanner(const char* function, bool withSizes, const char16_t* buffer, const char16_t* formatText, const void* source) + : name(function), sized(withSizes), start(buffer), input(buffer), format(formatText), args(source) {} int Run() { while (*format != 0) { @@ -162,6 +164,18 @@ public: } private: + [[noreturn]] void Fail(const std::string& message) const { + throw std::invalid_argument(std::string(name) + ": " + message); + } + + std::size_t IntegerSize(const std::string& length) const { + if (length.empty()) return sizeof(std::int32_t); + if (length == "hh") return sizeof(std::int8_t); + if (length == "h") return sizeof(std::int16_t); + if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); + Fail("invalid integer length modifier " + length); + } + void SkipSpace() { while (*input != 0 && IsSpace(*input)) ++input; } @@ -185,7 +199,7 @@ private: const std::size_t limit = (std::numeric_limits::max() - digit) / 10; directive.width = directive.width > limit ? std::numeric_limits::max() : directive.width * 10 + digit; } - if (format != digits && directive.width == 0) throw std::invalid_argument("swscanf_s: zero field width"); + if (format != digits && directive.width == 0) Fail("zero field width"); if (*format == u'h' || *format == u'l') { directive.length += static_cast(*format++); if (*format == directive.length[0]) directive.length += static_cast(*format++); @@ -193,7 +207,7 @@ private: directive.length += static_cast(*format++); } directive.conversion = *format; - if (directive.conversion == 0) throw std::invalid_argument("swscanf_s: incomplete conversion specification"); + if (directive.conversion == 0) Fail("incomplete conversion specification"); ++format; if (directive.conversion == u'[') { directive.setExcluded = *format == u'^'; @@ -201,10 +215,10 @@ private: directive.setBegin = format; if (*format == u']') ++format; while (*format != 0 && *format != u']') ++format; - if (*format == 0) throw std::invalid_argument("swscanf_s: unterminated scanset"); + if (*format == 0) Fail("unterminated scanset"); directive.setEnd = format++; for (const char16_t* member = directive.setBegin + 1; member + 1 < directive.setEnd; ++member) { - if (*member == u'-') throw std::invalid_argument("swscanf_s: scanset range is implementation-defined"); + if (*member == u'-') Fail("scanset range is implementation-defined"); } } return directive; @@ -230,7 +244,7 @@ private: case u'c': case u'[': return Text(directive); case u'n': return Count(directive); - default: throw std::invalid_argument("swscanf_s: unsupported conversion " + std::to_string(directive.conversion)); + default: Fail("unsupported conversion " + std::to_string(directive.conversion)); } } @@ -271,7 +285,7 @@ private: Outcome Float(const Directive& directive) { static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64); if (!directive.length.empty() && directive.length != "l" && directive.length != "L") - throw std::invalid_argument("swscanf_s: invalid floating length modifier " + directive.length); + Fail("invalid floating length modifier " + directive.length); void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); @@ -312,12 +326,12 @@ private: Outcome Text(const Directive& directive) { if (!directive.length.empty() && directive.length != "l") - throw std::invalid_argument("swscanf_s: invalid string length modifier " + directive.length); + Fail("invalid string length modifier " + directive.length); void* target = nullptr; std::size_t capacity = 0; if (!directive.suppress) { target = args.Next(); - capacity = args.Next(); + if (sized) capacity = args.Next(); if (target == nullptr) return Outcome::Violation; } if (directive.conversion == u's') SkipSpace(); @@ -343,7 +357,7 @@ private: } const std::size_t terminator = directive.conversion == u'c' ? 0 : 1; if (directive.length == "l") { - if (item.size() + terminator > capacity) return Truncated(target, capacity, true); + if (sized && item.size() + terminator > capacity) return Truncated(target, capacity, true); std::memcpy(target, item.data(), item.size() * sizeof(char16_t)); if (terminator != 0) static_cast(target)[item.size()] = 0; } else { @@ -351,10 +365,10 @@ private: for (const char16_t unit : item) { char byte[8]; const std::size_t written = wcrtomb_nid_postfix(byte, unit, nullptr); - if (written == static_cast(-1)) throw std::invalid_argument("swscanf_s: wide character has no single-byte form"); + if (written == static_cast(-1)) Fail("wide character has no single-byte form"); bytes.append(byte, written); } - if (bytes.size() + terminator > capacity) return Truncated(target, capacity, false); + if (sized && bytes.size() + terminator > capacity) return Truncated(target, capacity, false); std::memcpy(target, bytes.data(), bytes.size()); if (terminator != 0) static_cast(target)[bytes.size()] = 0; } @@ -364,7 +378,7 @@ private: } Outcome Count(const Directive& directive) { - if (directive.suppress || directive.width != 0) throw std::invalid_argument("swscanf_s: %n with suppression or width"); + if (directive.suppress || directive.width != 0) Fail("%n with suppression or width"); const std::size_t size = IntegerSize(directive.length); void* target = args.Next(); if (target == nullptr) return Outcome::Violation; @@ -378,29 +392,28 @@ private: extern "C" { -int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { +int APS5_VABI swscanf_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { if (buffer == nullptr || format == nullptr) return EOF; -#ifdef _WIN32 - __builtin_sysv_va_list args; - __builtin_sysv_va_start(args, format); -#else - std::va_list args; - va_start(args, format); -#endif + APS5_VA_BEGIN(format); try { - const int result = WideScanner(buffer, format, args).Run(); -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + const int result = WideScanner("swscanf", false, buffer, format, args).Run(); + APS5_VA_END(); return result; } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + APS5_VA_END(); + throw; + } +} + +int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { + if (buffer == nullptr || format == nullptr) return EOF; + APS5_VA_BEGIN(format); + try { + const int result = WideScanner("swscanf_s", true, buffer, format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); throw; } } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index fa5b81cac0..8cd823acbe 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -11,6 +11,7 @@ extern "C" { std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t*, char16_t**, int); int APS5_VABI _Iswctype_nid_postfix(std::uint32_t, std::uint64_t); +int APS5_VABI swscanf_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI swscanf_s_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI iswalnum_nid_postfix(std::uint32_t); int APS5_VABI iswalpha_nid_postfix(std::uint32_t); @@ -155,9 +156,66 @@ static void CheckScan() { Require(Throws([&] { swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}); }), "unconvertible narrow %s throws"); } +static void CheckUnsizedScan() { + int first = 0; + int second = 0; + Require(swscanf_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "swscanf %d"); + Require(swscanf_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "swscanf %i with a hex prefix"); + Require(swscanf_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "swscanf suppressed %d"); + Require(swscanf_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "swscanf %d width"); + std::int64_t wide = 0; + Require(swscanf_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "swscanf %ld is 64-bit"); + short half = 0; + Require(swscanf_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "swscanf %hd"); + unsigned value = 0; + Require(swscanf_nid_postfix(u"0xg", u"%x", &value) == 0, "swscanf %x prefix without digits is a matching failure"); + + char16_t text[16] = {u'?'}; + Require(swscanf_nid_postfix(u"hello world", u"%ls", text) == 1 && std::u16string(text) == u"hello", "swscanf %ls"); + Require(swscanf_nid_postfix(u"7 a-long-word", u"%d%ls", &first, text) == 2 && first == 7 && std::u16string(text) == u"a-long-word", "swscanf %ls takes no size"); + Require(swscanf_nid_postfix(u"hello world", u"%3ls", text) == 1 && std::u16string(text) == u"hel", "swscanf %ls width"); + char narrow[8] = {}; + Require(swscanf_nid_postfix(u"abc def", u"%s%d", narrow, &first) == 1 && std::strcmp(narrow, "abc") == 0, "swscanf %s"); + char16_t pair[3] = {u'?', u'?', u'?'}; + Require(swscanf_nid_postfix(u"xyz", u"%2lc", pair) == 1 && pair[0] == u'x' && pair[1] == u'y' && pair[2] == u'?', "swscanf %2lc"); + Require(swscanf_nid_postfix(u"x", u"%2lc", pair) == 0, "swscanf %2lc short input"); + char letter = 0; + Require(swscanf_nid_postfix(u" q7", u"%c%d", &letter, &first) == 1 && letter == ' ', "swscanf %c does not skip white space"); + Require(swscanf_nid_postfix(u"cab12", u"%l[abc]%d", text, &first) == 2 && std::u16string(text) == u"cab" && first == 12, "swscanf %l["); + Require(swscanf_nid_postfix(u"été!", u"%l[^!]", text) == 1 && std::u16string(text) == u"été", "swscanf %l[^"); + Require(swscanf_nid_postfix(u"key=val", u"%[^=]=%s", narrow, narrow + 4) == 2 && std::strcmp(narrow, "key") == 0 && std::strcmp(narrow + 4, "val") == 0, "swscanf %[^"); + std::int64_t count = -1; + Require(swscanf_nid_postfix(u" 12 ab", u"%d %ln", &first, &count) == 1 && count == 4, "swscanf %ln"); + int shortCount = -1; + Require(swscanf_nid_postfix(u"abc", u"%*ls%n", &shortCount) == 0 && shortCount == 3, "swscanf %n after a suppressed conversion"); + Require(swscanf_nid_postfix(u"50% off", u"%d%% off", &first) == 1 && first == 50, "swscanf %%"); + Require(swscanf_nid_postfix(u"50 off", u"%d%%", &first) == 1, "swscanf %% mismatch is a matching failure"); + + double real = 0; + Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); + Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + long double extended = 0; + Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); + float single = 0; + Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); + + Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); + Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); + Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); + Require(swscanf_nid_postfix(u" ", u"%ls", text) == -1, "swscanf white space only is an input failure"); + Require(swscanf_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "swscanf input failure after a conversion"); + Require(swscanf_nid_postfix(nullptr, u"%d", &first) == -1, "swscanf null buffer"); + Require(swscanf_nid_postfix(u"5", nullptr) == -1, "swscanf null format"); + Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); + Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); + Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); +} + int main() { CheckStoul(); CheckClassification(); CheckScan(); + CheckUnsizedScan(); return failures == 0 ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 740c9c59c1..0a6b4a8324 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,7 +62,7 @@ - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested From 3915f6dc09313a47fc09674a58f27352f954b124 Mon Sep 17 00:00:00 2001 From: Anshika Gupta Date: Sun, 11 Oct 2026 23:29:46 +0530 Subject: [PATCH 198/353] fix(libkernel): serialize positioned file transfers on Windows --- core/libs/prx/libkernel/File/src/Stdio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2427c016f8..dabe8fa285 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -170,6 +171,7 @@ static int NativeSync(int descriptor) { static int NativeFlock(int descriptor, int operation) { return File::Flock(descriptor, operation); } +static std::mutex g_positionedMutex; std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); @@ -183,6 +185,7 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ errno = EBADF; return -1; } + std::lock_guard lock(g_positionedMutex); LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { errno = ESPIPE; @@ -209,6 +212,7 @@ static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); } + std::lock_guard lock(g_positionedMutex); if (write) { const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); if (result != -2) return result; From 3cab3c05ec865b14950d1901ffa971ca80a178fc Mon Sep 17 00:00:00 2001 From: ducloc080202 Date: Tue, 13 Oct 2026 15:39:31 +0700 Subject: [PATCH 199/353] fix(libkernel): use per-descriptor locks for positioned transfers and seeks --- core/libs/prx/libkernel/File/include/File.hpp | 3 ++ .../libkernel/File/src/DescriptorControl.cpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 3 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 28 +++++++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index dc3b9e654e..4a14063e0e 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -27,6 +28,8 @@ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { int HostStandardError(); +[[nodiscard]] std::unique_lock LockPositionedTransfer(int descriptor); +void SharePositionedTransfer(int from, int to); } #endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index c52bdae4cd..b971311c0e 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -564,6 +564,7 @@ void ShareDescriptor(int from, int to) { std::lock_guard lock(g_mutex); auto status = Materialize(from).status; g_entries[to] = Entry{false, std::move(status)}; + File::SharePositionedTransfer(from, to); #ifdef _WIN32 GuestFileLockShare_nid_no_patch(from, to); #endif diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0868d227ca..8811f3e4b1 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -44,6 +44,7 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return descriptor; } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { + const auto positioned = File::LockPositionedTransfer(fd); const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); if (result != -2) return result; const auto position = ::_lseeki64(fd, offset, whence); @@ -55,6 +56,7 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } + const auto positioned = File::LockPositionedTransfer(fd); const int result = ::_read(fd, buf, static_cast(n)); if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; return result; @@ -63,6 +65,7 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } + const auto positioned = File::LockPositionedTransfer(fd); const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); if (shared != -2) return static_cast(shared); const int result = ::_write(fd, buf, static_cast(n)); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index dabe8fa285..f2027dd101 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -26,10 +26,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -171,7 +173,27 @@ static int NativeSync(int descriptor) { static int NativeFlock(int descriptor, int operation) { return File::Flock(descriptor, operation); } -static std::mutex g_positionedMutex; +struct PositionedLock { + std::mutex mutex; +}; +std::mutex g_positionedTableLock; +std::unordered_map> g_positionedLocks; +std::unique_lock File::LockPositionedTransfer(int descriptor) { + std::shared_ptr entry; + { + std::lock_guard table(g_positionedTableLock); + auto& slot = g_positionedLocks[descriptor]; + if (!slot) slot = std::make_shared(); + entry = slot; + } + return std::unique_lock(entry->mutex); +} +void File::SharePositionedTransfer(int from, int to) { + std::lock_guard table(g_positionedTableLock); + auto& source = g_positionedLocks[from]; + if (!source) source = std::make_shared(); + g_positionedLocks[to] = source; +} std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); @@ -185,7 +207,7 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ errno = EBADF; return -1; } - std::lock_guard lock(g_positionedMutex); + const auto positioned = File::LockPositionedTransfer(descriptor); LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { errno = ESPIPE; @@ -212,7 +234,7 @@ static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); } - std::lock_guard lock(g_positionedMutex); + const auto positioned = File::LockPositionedTransfer(descriptor); if (write) { const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); if (result != -2) return result; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a1bc73db68..2c6dd4e1bc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -111,7 +111,7 @@ - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. 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, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; 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, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console +- [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 - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist - [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`) From 0b46b3080a61bba279974c2ad5db8c8d32cadeb1 Mon Sep 17 00:00:00 2001 From: ducloc080202 Date: Tue, 13 Oct 2026 15:39:31 +0700 Subject: [PATCH 200/353] test(libkernel): cover concurrent pread against seek and read --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++ core/libs/tests/GuestPositionedRace.cpp | 77 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/libs/tests/GuestPositionedRace.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 72ce92342a..561975af14 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -292,6 +292,15 @@ if(BUILD_TESTING) set_tests_properties(guest_aio PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_positioned_race_tests ${LIBS_INCLUDE_DIR}/tests/GuestPositionedRace.cpp) + target_include_directories(guest_positioned_race_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_positioned_race_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_positioned_race_tests) + add_test(NAME guest_positioned_race COMMAND guest_positioned_race_tests) + if(WIN32) + set_tests_properties(guest_positioned_race PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestOnce.cpp) target_include_directories(guest_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_once_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestPositionedRace.cpp b/core/libs/tests/GuestPositionedRace.cpp new file mode 100644 index 0000000000..6c936c56b4 --- /dev/null +++ b/core/libs/tests/GuestPositionedRace.cpp @@ -0,0 +1,77 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelPread(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Positioned race check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +int main() { + const auto root = std::filesystem::path("anyps5-positioned-race-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "blocks.bin"; + constexpr std::int64_t kBlocks = 8; + constexpr std::size_t kBlockSize = 4096; + { + std::ofstream stream(file, std::ios::binary); + Require(static_cast(stream)); + for (std::int64_t block = 0; block < kBlocks; ++block) { + stream << std::string(kBlockSize, static_cast('A' + block)); + } + } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + std::array expected{}; + expected.fill('A'); + std::atomic stop{false}; + std::atomic misread{false}; + std::array seeked{}; + seeked.fill('H'); + std::thread seeker([&] { + std::array sink{}; + while (!stop.load()) { + if (sceKernelLseek(fd, (kBlocks - 1) * kBlockSize, 0) != (kBlocks - 1) * kBlockSize) { + misread = true; + return; + } + if (sceKernelRead(fd, sink.data(), sink.size()) != static_cast(sink.size()) || + std::memcmp(sink.data(), seeked.data(), seeked.size()) != 0) { + misread = true; + return; + } + } + }); + for (int i = 0; i < 5000 && !misread.load(); ++i) { + std::array bytes{}; + const auto got = sceKernelPread(fd, bytes.data(), bytes.size(), 0); + if (got != static_cast(bytes.size()) || std::memcmp(bytes.data(), expected.data(), expected.size()) != 0) { + misread = true; + } + } + stop = true; + seeker.join(); + Require(!misread); + Require(sceKernelClose(fd) == 0); + Require(std::filesystem::remove_all(root) > 0); +} From 296ff64ae9471699118b02cd82a6a66a77328604 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 11:18:55 +1300 Subject: [PATCH 201/353] fix(libkernel): share positioned transfer locks on Windows only The per-descriptor lock exists only in the Windows transfer path, so ShareDescriptor calls SharePositionedTransfer under _WIN32 like GuestFileLockShare. Linux failed to link libkernel with an undefined reference to it. --- core/libs/prx/libkernel/File/src/DescriptorControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index b971311c0e..ae0849bb98 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -564,8 +564,8 @@ void ShareDescriptor(int from, int to) { std::lock_guard lock(g_mutex); auto status = Materialize(from).status; g_entries[to] = Entry{false, std::move(status)}; - File::SharePositionedTransfer(from, to); #ifdef _WIN32 + File::SharePositionedTransfer(from, to); GuestFileLockShare_nid_no_patch(from, to); #endif } From 32fdaa65ae78169e8ab7232093773d159278586f Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 09:44:23 +0400 Subject: [PATCH 202/353] fix(libc): make flock advisory on Windows so other descriptors can read and write --- core/libs/prx/libc/src/FileLock.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileLock.cpp b/core/libs/prx/libc/src/FileLock.cpp index 1ed2279816..40efecd962 100644 --- a/core/libs/prx/libc/src/FileLock.cpp +++ b/core/libs/prx/libc/src/FileLock.cpp @@ -15,6 +15,8 @@ constexpr int LockShared = 1; constexpr int LockExclusive = 2; constexpr int LockNonblocking = 4; constexpr int LockUnlock = 8; +constexpr DWORD LockByteLow = 0xFFFFFFFE; +constexpr DWORD LockByteHigh = 0x7FFFFFFF; struct LockState { std::mutex mutex; @@ -55,14 +57,18 @@ int GuestFileLock_nid_no_patch(int fd, int operation) { if (state->mode == mode) return 0; if (state->mode != 0) { OVERLAPPED overlapped{}; - if (!::UnlockFileEx(handle, 0, MAXDWORD, MAXDWORD, &overlapped)) return -1; + overlapped.Offset = LockByteLow; + overlapped.OffsetHigh = LockByteHigh; + if (!::UnlockFileEx(handle, 0, 1, 0, &overlapped)) return -1; state->mode = 0; } if (mode == LockUnlock) return 0; DWORD flags = mode == LockExclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0; if (operation & LockNonblocking) flags |= LOCKFILE_FAIL_IMMEDIATELY; OVERLAPPED overlapped{}; - if (!::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped)) return -1; + overlapped.Offset = LockByteLow; + overlapped.OffsetHigh = LockByteHigh; + if (!::LockFileEx(handle, flags, 0, 1, 0, &overlapped)) return -1; state->mode = mode; return 0; } From b962be85363673807b977dff37a5e1929acbde7c Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 09:47:47 +0400 Subject: [PATCH 203/353] test(libc): read and write through another descriptor while a flock is held --- core/libs/tests/GuestFileLock.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index eea4421195..5cd80d4496 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -20,6 +20,9 @@ int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI flock_nid_postfix(int, int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int* APS5_VABI __error_nid_postfix(); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); FileStream* APS5_VABI fdopen_nid_postfix(int, const char*); @@ -165,6 +168,12 @@ static void Conversions() { Require(flock_nid_postfix(copy, 2 | 4) == -1 && *__error_nid_postfix() == 35); Require(flock_nid_postfix(fixture.other, 8) == 0); Require(Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + char byte = 'x'; + Require(sceKernelPwrite(fixture.other, &byte, 1, 0) == 1 && sceKernelWrite(fixture.other, "yz", 2) == 2); + Require(sceKernelPread(fixture.other, &byte, 1, 1) == 1 && byte == 'z'); + Require(flock_nid_postfix(fixture.other, 1 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(fixture.source, 8) == 0); } } From 773e7f218c96bdb3a349e223c055346dee82ce48 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 13 Oct 2026 09:47:52 +0400 Subject: [PATCH 204/353] docs(debt): describe the byte the Windows flock locks --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0a6b4a8324..56ba24d8a9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -63,7 +63,7 @@ - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title -- [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. +- [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. From 6ffc99bedbb5b827e616988f89229c2361482e8c Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:03:10 +0200 Subject: [PATCH 205/353] perf(agc): take the memory-value dword fill kernel as a buffer fill The 9-word kernel #3816 matches stores one dword per thread, loaded with s_buffer_load_dword through the descriptor in user[4..7]. Run as a compute dispatch, its stores are pending writes of unknown value, so a later read of DCC keys in that range waits for the GPU. fillBuffer now takes it as a uniform buffer fill when all of these hold: - the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22), so buffer_store_format_x stores the loaded dword unchanged; - the value descriptor covers the loaded dword: num_records times the stride (num_records alone when the stride is 0) is at least 4 bytes, the range the recompiler gives a scalar buffer load; - exactly 8 user SGPRs and group id X enabled, so the group id is s8; - a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z, and a 16-byte aligned range of whole 16-byte units. The value is read when the fill is recorded, after the labels queued before it, as the pattern-fill kernel's source is. When the value is one byte repeated and that byte is a DCC key code, the fill goes into the GPU key-store memo whatever ClassifyFill reports: the memo answers a key read only while this fill is still the newest pending write over the keys, and a fill over a color target's keys is not a storage image's keys fill. Co-Authored-By: Claude Opus 5.5 --- .../Driver/Memory/DwordPatternFill.hpp | 5 + .../src/Driver/Memory/BufferFill.cpp | 55 ++++-- .../tests/execution/MemoryValueFill.cpp | 180 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp index 025aa35c42..86e8d091a9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -2,6 +2,8 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP #include "Recompiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include #include #include @@ -19,6 +21,9 @@ struct DwordPatternFill { std::optional MatchDwordPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional MatchZeroDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +bool IsByteFill(const std::array& pattern); +std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill); } 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 4d5f80f1f2..c616422765 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -78,6 +78,32 @@ std::optional MatchZeroDwordFill(std::span(dwords * 4u), {}}; } +std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; + if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt; + const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]}); + if (!fill) return std::nullopt; + if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt; + return fill; +} + +bool IsByteFill(const std::array& pattern) { + return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); +} + +std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill) { + if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt; + switch (pattern[0] & 0xffu) { + case 0x00: return Graphics::DccKeys::Clear0000; + case 0x40: return Graphics::DccKeys::Clear0001; + case 0x80: return Graphics::DccKeys::Clear1110; + case 0xc0: return Graphics::DccKeys::Clear1111; + case 0x20: return Graphics::DccKeys::ClearRegister; + case 0xff: return Graphics::DccKeys::Uncompressed; + default: return std::nullopt; + } +} + bool Driver::fillClearEnabled() { static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr; return enabled; @@ -145,7 +171,9 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice) { auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute); if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute); - if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false; + std::optional memoryFill; + if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute); + if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false; std::uint64_t base; std::size_t bytes; std::array pattern; @@ -154,6 +182,15 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanbase; bytes = dwordFill->bytes; pattern = dwordFill->pattern; + } else if (memoryFill) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(memoryFill->valueAddress), 4)) return false; + GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4); + std::fill(pattern.begin() + 1, pattern.end(), pattern[0]); + base = memoryFill->address; + bytes = memoryFill->bytes; } else if (MatchesPatternFillKernel(code, userData, compute)) { const auto range = DecodePatternFillRange(userData, packet); if (!range) return false; @@ -222,7 +259,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId); - const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); + const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern); if (uniformKeysFill) { Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast(pattern[0])); Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast(pattern[0])); @@ -255,18 +292,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanFillBuffer(base, bytes, pattern); phase(FillDevice); - if (uniformKeysFill && stored && !cleared) { - Graphics::DccKeys filled = Graphics::DccKeys::Mixed; - switch (pattern[0] & 0xffu) { - case 0x00: filled = Graphics::DccKeys::Clear0000; break; - case 0x40: filled = Graphics::DccKeys::Clear0001; break; - case 0x80: filled = Graphics::DccKeys::Clear1110; break; - case 0xc0: filled = Graphics::DccKeys::Clear1111; break; - case 0x20: filled = Graphics::DccKeys::ClearRegister; break; - case 0xff: filled = Graphics::DccKeys::Uncompressed; break; - default: break; - } - if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled); + if (stored && !cleared) { + if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled); } if (profile && stored && !cleared) { const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp new file mode 100644 index 0000000000..eece8a90ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::KeysFillMemo; +using AgcDriver::DriverDetail::MatchMemoryValueBufferFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format32Uint = 0x00014004u; +alignas(256) std::array Value{}; +alignas(256) std::array Output{}; + +void KeysFillMemoTests() { + using AgcDriver::Graphics::DccKeys; + const auto bytes = [](std::uint32_t byte) { const auto word = byte * 0x01010101u; return std::array{word, word, word, word}; }; + const std::array, 6> codes{{{0x00u, DccKeys::Clear0000}, {0x40u, DccKeys::Clear0001}, {0x80u, DccKeys::Clear1110}, {0xc0u, DccKeys::Clear1111}, {0x20u, DccKeys::ClearRegister}, {0xffu, DccKeys::Uncompressed}}}; + for (const auto& [byte, keys] : codes) { + Require(KeysFillMemo(bytes(byte), false, true) == keys, "a memory-value fill of a key code over keys that are no storage image's was not recorded"); + Require(KeysFillMemo(bytes(byte), true, false) == keys, "a fill of a key code over a storage image's keys was not recorded"); + Require(KeysFillMemo(bytes(byte), true, true) == keys, "a memory-value fill over a storage image's keys was not recorded"); + Require(!KeysFillMemo(bytes(byte), false, false), "a fill over keys that are no storage image's, other than a memory-value fill, was recorded"); + } + Require(!KeysFillMemo(bytes(0x12u), true, true), "a byte that is no key code was recorded"); + auto mixed = bytes(0x40u); + mixed[3] = 0x40404041u; + Require(!KeysFillMemo(mixed, true, true), "a pattern of two different words was recorded"); + mixed = bytes(0x40u); + mixed[0] = 0x40404000u; + Require(!KeysFillMemo(mixed, true, true), "a word of different bytes was recorded"); +} + +std::vector UserData(const std::uint32_t* output, std::uint32_t records, std::uint32_t valueRecords = 16, std::uint32_t valueStride = 0, std::uint32_t format = Format32Uint) { + const auto address = reinterpret_cast(output); + const auto value = reinterpret_cast(Value.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, static_cast(value), static_cast((value >> 32u) & 0xffffu) | (valueStride << 16u), valueRecords, Format32Uint}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + const auto data = UserData(Output.data(), 1024); + const auto fill = MatchMemoryValueBufferFill(packet, Code, data, compute); + Require(fill && fill->address == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->valueAddress == reinterpret_cast(Value.data()), "the memory-value fill kernel was not taken as a fill of 256 dwords"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u}) { + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute).has_value(), "a 32-bit output format was refused"); + } + for (const std::uint32_t format : {0x0000d004u, 0x00027facu, 0x00038004u}) { + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute), "an output format whose elements are not one 32-bit value matched"); + } + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 4), compute).has_value(), "a value descriptor of one 4-byte element was refused"); + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 4), compute).has_value(), "a value descriptor of 4 bytes was refused"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 0), compute), "a value descriptor without records matched (the load returns 0)"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 3), compute), "a value descriptor shorter than the loaded dword matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 2), compute), "a value descriptor of one 2-byte element matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchMemoryValueBufferFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchMemoryValueBufferFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchMemoryValueBufferFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + auto altered = Code; + altered[5] ^= 1u; + Require(!MatchMemoryValueBufferFill(packet, altered, data, compute), "a different shader matched"); + const auto clamped = MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 64), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the output descriptor's records"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups, std::uint32_t wave) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u | (wave == 32 ? 0x8000u : 0u)}); + return words; +} + +void Label(std::vector& words, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +void Submit(const std::vector& words) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); +} + +void Expect(std::size_t first, std::size_t count, std::uint32_t value, const std::string& what) { + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = i >= first && i < first + count ? value : Untouched; + Require(Output[i] == expected, what + ": word " + std::to_string(i) + " is " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } +} + +void ExecuteTests() { + struct Case { + std::uint32_t value, first, records, groups, valueRecords, valueStride, stored; + const char* what; + }; + constexpr std::array cases{{ + {0xffffffffu, 0, 256, 4, 16, 0, 0xffffffffu, "a 16-byte aligned fill of every launched thread"}, + {0u, 0, 256, 2, 16, 0, 0u, "a fill shorter than its records"}, + {0x40404040u, 0, 1000, 4, 16, 0, 0x40404040u, "a fill longer than its launched threads"}, + {0x0c0c0c0cu, 0, 256, 4, 1, 4, 0x0c0c0c0cu, "a fill whose value descriptor holds one 4-byte element"}, + {0x12345678u, 0, 255, 4, 16, 0, 0x12345678u, "a fill whose size is not a multiple of 16 bytes"}, + {0x80808080u, 1, 256, 4, 16, 0, 0x80808080u, "a fill whose start is not 16-byte aligned"}, + {0x20202020u, 0, 0, 4, 16, 0, 0x20202020u, "an empty fill"}, + {0xc0c0c0c0u, 0, 256, 4, 0, 0, 0u, "a fill whose value descriptor has no records (the load returns 0)"}, + }}; + for (const auto wave : {32u, 64u}) { + for (const auto& item : cases) { + Output.fill(Untouched); + Value[0] = item.value; + Submit(Commands(UserData(Output.data() + item.first, item.records, item.valueRecords, item.valueStride), item.groups, wave)); + Expect(item.first, std::min(item.records, item.groups * 64u), item.stored, std::string(item.what) + " (wave " + std::to_string(wave) + ")"); + } + } + Output.fill(Untouched); + Value[0] = 0x11111111u; + auto words = Commands(UserData(Output.data(), 256), 4, 64); + Label(words, &Value[0], 0xc0c0c0c0u); + const auto again = Commands(UserData(Output.data(), 128), 2, 64); + words.insert(words.end(), again.begin(), again.end()); + Submit(words); + for (std::size_t i = 0; i < 256; ++i) { + Require(Output[i] == (i < 128 ? 0xc0c0c0c0u : 0x11111111u), "a fill did not read the value a preceding GPU label stored, at word " + std::to_string(i)); + } +} + +} + +int main() { + try { + KeysFillMemoTests(); + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("memory-value fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8fbdd6a72b..0c171c38fc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -72,7 +72,7 @@ - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware -- [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. +- [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. From 1c958de097db9f31c50e49acdcfb644716ac555e Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 06:28:33 +0200 Subject: [PATCH 206/353] feat(recompiler): structurize refused control flow with reach flags When every attempt of the structurizer ladder refuses a graph, the title aborts. Rebuild such a graph from a hierarchical decomposition into strongly connected components instead: each original block keeps one copy and runs behind a guard on its own reach flag, and each component becomes a loop with a single latch that repeats it while a back edge set its again flag. Every edge sets the reach flag of its target, so exactly one flag names the next original block and the blocks run in the input's order for every branch decision. Irreducible and non-terminating graphs are handled the same way, and no block is cloned. The ladder runs first and its output is unchanged. The fallback only runs when both attempt series refuse; if it cannot structure the graph, the ladder's error is reported as before. Blocks that start the top-level order unconditionally run unguarded, so the entry block's results reach later blocks without a stale path. APS5_FORCE_STRUCTURIZER_FALLBACK=1 uses the construction for every supported graph, for tests. Co-Authored-By: Claude Opus 5.5 --- .../include/ControlFlow/Structurizer.hpp | 1 + .../ControlFlow/src/Structurizer.cpp | 396 +++++++++++++++++- docs/dev/debt/shader.md | 1 + 3 files changed, 397 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index 20cd18e3ad..ac92180cb2 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -11,6 +11,7 @@ public: private: void structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const; + void structurizeWithFlags(ControlFlowGraph& graph) const; void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const; void privatizeSharedReturns(ControlFlowGraph& graph) const; void computeDominatorTree(ControlFlowGraph& graph) const; diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index f699c1e3df..c108bed454 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1450,10 +1450,328 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { state.components.push_back(std::move(component)); } +bool forcedFlagStructurization() { + static const bool forced = std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr; + return forced; +} + +std::vector terminatorTargets(const Terminator& terminator) { + if (terminator.kind == TerminatorKind::Branch) { + return {terminator.trueBlock}; + } + if (terminator.kind == TerminatorKind::ConditionalBranch) { + return {terminator.trueBlock, terminator.falseBlock}; + } + return {}; +} + +bool supportsFlagStructurization(const ControlFlowGraph& graph) { + const auto count = graph.blocks.size(); + if (graph.unsupported || graph.entryBlock >= count) { + return false; + } + for (std::uint32_t id = 0; id < count; ++id) { + const auto& block = graph.blocks[id]; + const auto kind = block.terminator.kind; + if (block.id != id || (kind != TerminatorKind::Branch && kind != TerminatorKind::ConditionalBranch && kind != TerminatorKind::Return)) { + return false; + } + const auto targets = terminatorTargets(block.terminator); + if (std::any_of(targets.begin(), targets.end(), [&](std::uint32_t target) { return target >= count; })) { + return false; + } + } + return true; +} + +struct FlagEmission { + ControlFlowGraph* graph = nullptr; + std::uint32_t entryBlock = InvalidControlFlowId; + std::uint32_t firstVariable = 0; + std::uint32_t nextVariable = 0; + std::vector> targets; + std::vector> successors; + std::vector> predecessors; + std::vector> openLoops; + std::vector order; + std::vector fallthroughs; +}; + +std::vector> orderedComponents(const FlagEmission& emission, const std::vector& members) { + const auto count = emission.successors.size(); + std::vector inside(count, false); + for (const auto member : members) { + inside[member] = true; + } + std::vector index(count, InvalidControlFlowId); + std::vector lowlink(count, InvalidControlFlowId); + std::vector componentOf(count, InvalidControlFlowId); + std::vector onStack(count, false); + std::vector stack; + std::vector> work; + std::vector> components; + std::uint32_t nextIndex = 0; + const auto visit = [&](std::uint32_t blockId) { + index[blockId] = nextIndex; + lowlink[blockId] = nextIndex; + ++nextIndex; + stack.push_back(blockId); + onStack[blockId] = true; + work.emplace_back(blockId, 0u); + }; + for (const auto root : members) { + if (index[root] != InvalidControlFlowId) { + continue; + } + visit(root); + while (!work.empty()) { + const auto blockId = work.back().first; + const auto& successors = emission.successors[blockId]; + if (work.back().second < successors.size()) { + const auto successor = successors[work.back().second++]; + if (!inside[successor]) { + continue; + } + if (index[successor] == InvalidControlFlowId) { + visit(successor); + } else if (onStack[successor]) { + lowlink[blockId] = std::min(lowlink[blockId], index[successor]); + } + continue; + } + work.pop_back(); + if (!work.empty()) { + auto& parent = lowlink[work.back().first]; + parent = std::min(parent, lowlink[blockId]); + } + if (lowlink[blockId] != index[blockId]) { + continue; + } + std::vector component; + for (;;) { + const auto member = stack.back(); + stack.pop_back(); + onStack[member] = false; + componentOf[member] = static_cast(components.size()); + component.push_back(member); + if (member == blockId) { + break; + } + } + sortUnique(component); + components.push_back(std::move(component)); + } + } + + std::vector incoming(components.size(), 0u); + std::vector> following(components.size()); + for (const auto member : members) { + for (const auto successor : emission.successors[member]) { + if (inside[successor] && componentOf[successor] != componentOf[member]) { + following[componentOf[member]].push_back(componentOf[successor]); + } + } + } + for (auto& next : following) { + sortUnique(next); + for (const auto component : next) { + ++incoming[component]; + } + } + std::map ready; + for (std::uint32_t component = 0; component < components.size(); ++component) { + if (incoming[component] == 0u) { + ready.emplace(components[component].front(), component); + } + } + std::vector> ordered; + ordered.reserve(components.size()); + while (!ready.empty()) { + const auto component = ready.begin()->second; + ready.erase(ready.begin()); + for (const auto next : following[component]) { + if (--incoming[next] == 0u) { + ready.emplace(components[next].front(), next); + } + } + ordered.push_back(std::move(components[component])); + } + return ordered; +} + +std::uint32_t flagLoopHead(const FlagEmission& emission, const std::vector& component) { + for (const auto member : component) { + const auto& predecessors = emission.predecessors[member]; + if (member == emission.entryBlock || std::any_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return !std::binary_search(component.begin(), component.end(), predecessor); })) { + return member; + } + } + return component.front(); +} + +std::uint32_t appendFlagBlock(FlagEmission& emission, std::uint32_t positionBlock) { + const auto block = appendRouteBlock(*emission.graph, positionBlock); + emission.order.push_back(block); + return block; +} + +std::uint32_t appendFlagAssignment(FlagEmission& emission, std::uint32_t positionBlock, std::uint32_t variable, std::int32_t value) { + const auto assignment = appendAssignmentBlock(*emission.graph, positionBlock, positionBlock, variable, value); + emission.order.push_back(assignment); + return assignment; +} + +void setDispatch(BasicBlock& block, std::uint32_t variable, std::uint32_t trueTarget, std::uint32_t falseTarget) { + block.successors = {trueTarget, falseTarget}; + block.terminator.kind = TerminatorKind::ConditionalBranch; + block.terminator.condition = BranchCondition::GotoVariable; + block.terminator.trueBlock = trueTarget; + block.terminator.falseBlock = falseTarget; + block.terminator.gotoVariable = variable; +} + +std::vector alwaysNextComponents(const FlagEmission& emission, const std::vector>& components, const std::vector& firsts) { + const auto count = static_cast(components.size()); + std::vector componentOf(emission.successors.size(), InvalidControlFlowId); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + componentOf[member] = component; + } + } + std::vector next(count, true); + std::vector skipped(count + 1u, 0); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + for (const auto successor : emission.successors[member]) { + const auto target = componentOf[successor]; + if (target <= component) { + continue; + } + ++skipped[component + 1u]; + --skipped[target]; + if (successor != firsts[target]) { + next[target] = false; + } + } + } + } + const auto entryComponent = componentOf[emission.entryBlock]; + std::int64_t crossing = 0; + for (std::uint32_t component = 0; component < count; ++component) { + crossing += skipped[component]; + if (crossing > 0 || component < entryComponent || (component == entryComponent && firsts[component] != emission.entryBlock)) { + next[component] = false; + } + } + return next; +} + +void emitFlagBlock(FlagEmission& emission, std::uint32_t original, bool guarded) { + auto& graph = *emission.graph; + const auto reach = emission.firstVariable + original; + const auto guard = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto clear = appendFlagAssignment(emission, original, reach, 0); + emission.fallthroughs.push_back(clear); + emission.order.push_back(original); + + std::vector edges; + std::vector ends; + for (const auto target : emission.targets[original]) { + const auto edge = appendFlagAssignment(emission, original, emission.firstVariable + target, 1); + edges.push_back(edge); + const auto loop = std::find_if(emission.openLoops.begin(), emission.openLoops.end(), [&](const std::pair& open) { return open.first == target; }); + if (loop == emission.openLoops.end()) { + ends.push_back(edge); + continue; + } + emission.fallthroughs.push_back(edge); + ends.push_back(appendFlagAssignment(emission, original, loop->second, 1)); + } + + const bool conditional = graph.blocks[original].terminator.kind == TerminatorKind::ConditionalBranch; + const auto join = conditional ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto merge = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto exit = conditional ? join : merge; + for (const auto end : ends) { + if (exit == InvalidControlFlowId) { + emission.fallthroughs.push_back(end); + } else { + setBranch(graph.blocks[end], exit); + } + } + if (conditional) { + emission.fallthroughs.push_back(join); + } + if (guarded) { + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[guard], reach, clear, merge); + graph.blocks[guard].terminator.mergeBlock = merge; + } + + auto& block = graph.blocks[original]; + block.successors = edges; + block.terminator.mergeBlock = join; + block.terminator.continueBlock = InvalidControlFlowId; + block.terminator.loopHeader = false; + if (!edges.empty()) { + block.terminator.trueBlock = edges.front(); + } + if (conditional) { + block.terminator.falseBlock = edges.back(); + } +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members); + +void emitFlagLoop(FlagEmission& emission, std::vector component, std::uint32_t head, bool guarded) { + auto& graph = *emission.graph; + const auto again = emission.nextVariable++; + const auto header = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(header); + emission.fallthroughs.push_back(appendFlagAssignment(emission, head, again, 0)); + emission.openLoops.emplace_back(head, again); + emitFlagBlock(emission, head, guarded); + component.erase(std::find(component.begin(), component.end(), head)); + emitFlagRegion(emission, component); + emission.openLoops.pop_back(); + const auto latch = appendFlagBlock(emission, head); + const auto merge = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[latch], again, header, merge); + auto& terminator = graph.blocks[header].terminator; + terminator.loopHeader = true; + terminator.mergeBlock = merge; + terminator.continueBlock = latch; +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members) { + const auto components = orderedComponents(emission, members); + std::vector loops; + std::vector firsts; + for (const auto& component : components) { + const auto first = component.front(); + loops.push_back(component.size() != 1u || contains(emission.successors[first], first)); + firsts.push_back(loops.back() ? flagLoopHead(emission, component) : first); + } + const auto next = emission.openLoops.empty() ? alwaysNextComponents(emission, components, firsts) : std::vector(components.size(), false); + for (std::size_t index = 0; index < components.size(); ++index) { + if (loops[index]) { + emitFlagLoop(emission, components[index], firsts[index], !next[index]); + } else { + emitFlagBlock(emission, firsts[index], !next[index] || emission.graph->blocks[firsts[index]].terminator.kind == TerminatorKind::Return); + } + } +} + } void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; + const bool flagsSupported = supportsFlagStructurization(original); + if (flagsSupported && forcedFlagStructurization()) { + structurizeWithFlags(graph); + return; + } try { structurizeAttempts(graph, false); } catch (const std::runtime_error&) { @@ -1462,11 +1780,87 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { try { structurizeAttempts(graph, true); } catch (const std::runtime_error&) { - std::rethrow_exception(error); + if (!flagsSupported) { + std::rethrow_exception(error); + } + graph = original; + try { + structurizeWithFlags(graph); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } } } } +void Structurizer::structurizeWithFlags(ControlFlowGraph& graph) const { + if (!supportsFlagStructurization(graph)) { + throw std::runtime_error("CFG flag structurization supports only branch, conditional branch and return blocks"); + } + const auto count = static_cast(graph.blocks.size()); + const auto entry = graph.entryBlock; + FlagEmission emission; + emission.graph = &graph; + emission.entryBlock = entry; + emission.firstVariable = nextGotoVariable(graph); + emission.nextVariable = emission.firstVariable + count; + emission.targets.resize(count); + emission.successors.resize(count); + emission.predecessors.resize(count); + for (const auto& block : graph.blocks) { + emission.targets[block.id] = terminatorTargets(block.terminator); + emission.successors[block.id] = emission.targets[block.id]; + sortUnique(emission.successors[block.id]); + for (const auto successor : emission.successors[block.id]) { + emission.predecessors[successor].push_back(block.id); + } + } + + emitFlagRegion(emission, allBlockIds(count)); + const auto body = std::move(emission.order); + emission.order.clear(); + for (auto variable = emission.firstVariable; variable < emission.nextVariable; ++variable) { + emission.fallthroughs.push_back(appendFlagAssignment(emission, entry, variable, variable == emission.firstVariable + entry ? 1 : 0)); + } + emission.order.insert(emission.order.end(), body.begin(), body.end()); + appendFlagBlock(emission, entry); + + const auto& order = emission.order; + std::vector placed(graph.blocks.size(), false); + for (const auto blockId : order) { + if (blockId >= placed.size() || placed[blockId]) { + throw std::runtime_error("CFG flag structurization placed a block twice"); + } + placed[blockId] = true; + } + if (order.size() != graph.blocks.size()) { + throw std::runtime_error("CFG flag structurization did not place every block"); + } + std::vector fallthrough(graph.blocks.size(), false); + for (const auto blockId : emission.fallthroughs) { + fallthrough[blockId] = true; + } + for (std::size_t position = 0; position + 1u < order.size(); ++position) { + if (fallthrough[order[position]]) { + setBranch(graph.blocks[order[position]], order[position + 1u]); + } + } + + graph.entryBlock = order.front(); + std::vector blocks; + blocks.reserve(order.size()); + for (const auto blockId : order) { + blocks.push_back(std::move(graph.blocks[blockId])); + } + applyBlockOrder(graph, std::move(blocks)); + rebuildPredecessors(graph); + recomputeAnalyses(graph); + orderByDominance(graph); + if (graph.irreducible || !hasStructuredConstructs(graph)) { + throw std::runtime_error("CFG flag structurization did not produce structured constructs"); + } +} + void Structurizer::structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const { const auto original = graph; try { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7163d55302..205a66251c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -114,3 +114,4 @@ - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. +- [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first, so a graph on which they spend seconds before refusing is not prepared faster. `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests From 22c0c517305fcfe0806c1c0947742c6fa8f56d7c Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 06:28:33 +0200 Subject: [PATCH 207/353] test(recompiler): cover refused control flow and the forced structurizer fallback Structurize eight graphs the ladder refuses on main (a duplicate merge, a region that wraps around to its dominator, the merge-splitting budget, a conditional without a structured merge, an irreducible entry and three loops that never exit) and the #2642 two-latch loop, which the ladder accepts. Each must give structured branches, every original block exactly once and the same executions as the input for every branch decision. Register agc_structurizer_forced_fallback and agc_shader_control_flow_forced_fallback, which run the in-tree graphs and RDNA programs, through Recompile and SPIR-V validation, with APS5_FORCE_STRUCTURIZER_FALLBACK=1. Under that switch the split-kind checks become checks that no block is cloned and a route variable is used, and the nested-selection check names blocks by their original instructions instead of their position. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 42 ++++++--- .../libSceAgcDriver/tests/Structurizer.cpp | 89 ++++++++++++++++++- 3 files changed, 124 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8c49c9589c..9efa3c282c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1305,8 +1305,12 @@ if(BUILD_TESTING) set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests) set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_forced_fallback COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_forced_fallback COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests) set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a78c75f9bc..5d64507559 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -342,16 +343,10 @@ void verifyLargeLoopExitFallback() { static_cast(recompile(name, code, 64u)); } -void verifyNestedExitForwarders() { +std::vector branchProgram(std::span> targets, std::span order) { constexpr auto none = InvalidControlFlowId; - constexpr std::array, 21> targets{{ - {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, - {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, - {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, - }}; - constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; std::vector code; - std::array starts{}; + std::vector starts(targets.size()); std::vector> patches; for (std::size_t at = 0; at < order.size(); ++at) { const auto block = order[at]; @@ -377,19 +372,45 @@ void verifyNestedExitForwarders() { } for (const auto& [instruction, target] : patches) { const auto displacement = static_cast(starts[target]) - static_cast(instruction) - 1; - require(displacement >= -32768 && displacement <= 32767, "nested exit forwarder test branch is out of range"); + require(displacement >= -32768 && displacement <= 32767, "a generated test branch is out of range"); code[instruction] |= static_cast(displacement) & 0xffffu; } + return code; +} + +void verifyNestedExitForwarders() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 21> targets{{ + {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, + {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, + {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, + }}; + constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; + const auto code = branchProgram(targets, order); const std::string name("nested exit forwarders keep conditional selections structured"); static_cast(verifyGraph(name, code)); static_cast(recompile(name, code, 32u)); static_cast(recompile(name, code, 64u)); } +void verifyIrreducibleLoopEntry() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 4> targets{{{1, 2}, {2, 3}, {1, 3}, {none, none}}}; + constexpr std::array order{0, 1, 2, 3}; + const auto code = branchProgram(targets, order); + const std::string name("a loop entered at either of its two blocks runs behind reach flags"); + const auto result = verifyGraph(name, code); + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + void verifyProgram(const Program& program) { const std::string name(program.name); const auto result = verifyGraph(name, program.code); - if (program.split == Split::None) { + if (std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr) { + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + } else if (program.split == Split::None) { require(result.clonedInstructions == 0 && result.routeVariables == 0, name + ": expected no clone or routing, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); } else if (program.split == Split::Clone) { require(result.clonedInstructions != 0 && result.routeVariables == 0, name + ": expected a clone, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); @@ -1281,6 +1302,7 @@ done: verifyNullSwappc(); verifyLargeLoopExitFallback(); verifyNestedExitForwarders(); + verifyIrreducibleLoopEntry(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index e8b1bbee05..6177ad842f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -174,6 +176,80 @@ void requireStructuredLoop(const std::vector>& succes requireOriginalBranches(graph, successors, name); } +const BasicBlock& originalCopy(const ControlFlowGraph& graph, std::uint32_t original) { + const auto copy = std::find_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return block.instructionBegin == original * 2 && block.instructionEnd == original * 2 + 2; }); + if (copy == graph.blocks.end()) throw std::runtime_error("original block " + std::to_string(original) + " is missing"); + return *copy; +} + +std::uint32_t guestBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes, const char* name) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) { + if (terminator.gotoValue >= 0) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " sets a route variable after guest instructions"); + return blockId; + } + if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0; + if (terminator.kind == TerminatorKind::Branch) { + blockId = terminator.trueBlock; + } else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable) { + const auto route = routes.find(terminator.gotoVariable); + if (route == routes.end()) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " reads route variable " + std::to_string(terminator.gotoVariable) + " before any path sets it"); + blockId = route->second ? terminator.trueBlock : terminator.falseBlock; + } else { + return blockId; + } + } + throw std::runtime_error(std::string(name) + ": a cycle of empty blocks"); +} + +void requireSameExecutions(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + if (structured.blocks.empty() || structured.entryBlock != 0 || structured.blocks.front().id != 0) { + throw std::runtime_error(std::string(name) + ": the structured graph does not start at block 0"); + } + using State = std::tuple; + std::vector pending{{0u, original.entryBlock, {}}}; + std::set visited; + while (!pending.empty()) { + auto [structuredId, originalId, routes] = std::move(pending.back()); + pending.pop_back(); + const auto copyId = guestBlock(structured, structuredId, routes, name); + if (!visited.emplace(copyId, originalId, routes).second) continue; + const auto& copy = structured.FindBlock(copyId); + const auto& source = original.FindBlock(originalId); + if (copy.instructionBegin != source.instructionBegin || copy.instructionEnd != source.instructionEnd || copy.terminator.kind != source.terminator.kind || copy.terminator.condition != source.terminator.condition) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(copyId) + " does not run like original block " + std::to_string(originalId)); + } + if (source.terminator.kind == TerminatorKind::Branch || source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.trueBlock, source.terminator.trueBlock, routes); + if (source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.falseBlock, source.terminator.falseBlock, routes); + } +} + +void requireUnconditionalEntry(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + const auto& entry = original.FindBlock(original.entryBlock); + auto blockId = structured.blocks.front().id; + for (std::size_t step = 0; step <= structured.blocks.size(); ++step) { + const auto& block = structured.FindBlock(blockId); + if (block.instructionBegin == entry.instructionBegin && block.instructionEnd == entry.instructionEnd) return; + if (block.instructionBegin != block.instructionEnd || block.terminator.kind != TerminatorKind::Branch) break; + blockId = block.terminator.trueBlock; + } + throw std::runtime_error(std::string(name) + ": the entry block does not run unconditionally first"); +} + +void requireSameStructuredExecutions(const std::vector>& successors, const char* name) { + const auto original = makeGraph(successors); + auto graph = original; + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, name); + requireSameExecutions(original, graph, name); + requireUnconditionalEntry(original, graph, name); +} + std::vector> originalPostDominators(const ControlFlowGraph& graph) { std::vector all; for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); @@ -290,8 +366,8 @@ int main() { auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}); Structurizer{}.Structurize(nested); requireExactPostDominators(nested, "nested selections"); - if (nested.FindBlock(2).terminator.mergeBlock == nested.FindBlock(3).terminator.mergeBlock) { - std::fprintf(stderr, "the nested selections share merge block %u\n", nested.FindBlock(2).terminator.mergeBlock); + if (originalCopy(nested, 2).terminator.mergeBlock == originalCopy(nested, 3).terminator.mergeBlock) { + std::fprintf(stderr, "the nested selections share merge block %u\n", originalCopy(nested, 2).terminator.mergeBlock); return 1; } requireStructuredBranches(nested, "nested selections"); @@ -355,6 +431,15 @@ int main() { } Structurizer{}.Structurize(sharedDiscard); requireStructuredBranches(sharedDiscard, "early discards that share one ending block"); + requireSameStructuredExecutions({{1, 2}, {3, 0}, {}, {2}}, "an entry loop with two exits whose merges collide"); + requireSameStructuredExecutions({{2}, {}, {4, 1}, {1}, {2, 3}}, "a selection region that wraps around to its dominator"); + requireSameStructuredExecutions({{1}, {2, 3}, {0, 4}, {}, {3}}, "an entry loop whose merge splitting runs out of budget"); + requireSameStructuredExecutions({{2, 5}, {4, 1}, {1, 3}, {}, {}, {1}}, "a self-loop entered from both arms of the entry selection"); + requireSameStructuredExecutions({{1, 2}, {2, 3}, {1, 3}, {}}, "an irreducible cycle entered at both blocks"); + requireSameStructuredExecutions({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches compared run by run"); + requireSameStructuredExecutions({{1, 2}, {2, 0}, {1}}, "a cycle entered at both blocks inside a loop that never ends"); + requireSameStructuredExecutions({{1, 3}, {2}, {1}, {}}, "a branch that returns or enters a loop that never ends"); + requireSameStructuredExecutions({{1}, {1}}, "a block that branches to itself forever"); requireOriginalDominanceSets(makeGraph(diamonds), "raw diamond chain"); requireOriginalDominanceSets(makeGraph({{1}, {2, 3}, {1}, {}}), "a loop with an exit"); requireOriginalDominanceSets(makeGraph({{1}, {2}, {1}}), "a non-exiting SCC"); From 2c53270b94421eba7c1fe4537b6c91487787b1f2 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 08:44:10 +0200 Subject: [PATCH 208/353] perf(recompiler): iterate structurizer fixpoints in post-order and bound shared merge splitting growth The structurizer's attempts could spend more than 10 s on a graph of 20 blocks before they accepted or refused it; in a title that stalls shader preparation. Nearly all of that time went to recomputing dominators and post-dominator fixpoints while merge splitting grew the graph to hundreds or thousands of blocks. Dominators, post-dominators and the fixpoints in mergeBesideReturns, iterationSelectionMerge and joinWithinIteration now sweep the blocks in reverse post-order (dominators) or post-order (the others) instead of in id order. Splitting appends its new blocks at the end, so id order needed a sweep for almost every new block. Each fixpoint starts from the full set and its equations are monotone, so every sweep order reaches the same greatest fixed point and the result does not change. Shared merge splitting also refuses an attempt once the graph has more than 16 times its block count at the start of splitting (at least 64 blocks). Without it, splits that route through a new junction each round grew some graphs past 4,000 blocks before the round budget ran out. A refused attempt goes to the next attempt or to the reach-flag fallback, as it does after the round budget. Co-Authored-By: Claude Opus 5.5 --- .../ControlFlow/src/Structurizer.cpp | 78 ++++++++++++------- docs/dev/debt/shader.md | 2 +- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index c108bed454..c78d429445 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -22,6 +22,29 @@ std::vector allBlockIds(std::uint32_t count) { return ids; } +std::vector reversePostOrder(const ControlFlowGraph& graph); + +std::vector analysisOrder(const ControlFlowGraph& graph, bool successorsFirst) { + const auto count = static_cast(graph.blocks.size()); + std::vector order; + std::vector placed(count, false); + if (graph.entryBlock < count) { + order = reversePostOrder(graph); + if (successorsFirst) { + std::reverse(order.begin(), order.end()); + } + for (const auto id : order) { + placed[id] = true; + } + } + for (std::uint32_t id = 0; id < count; ++id) { + if (!placed[id]) { + order.push_back(id); + } + } + return order; +} + bool inPlaceIntersections() { static const bool enabled = std::getenv("APS5_NO_PERF_DOM_INTERSECTION") == nullptr; return enabled; @@ -316,43 +339,47 @@ bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& lo } std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t trueTarget, std::uint32_t falseTarget, std::uint32_t globalMerge) { - std::vector reached; + std::vector reached(graph.blocks.size(), false); std::vector pending = {trueTarget, falseTarget}; while (!pending.empty()) { const auto blockId = pending.back(); pending.pop_back(); - if (blockId == globalMerge || contains(reached, blockId) || !isInsideLoopConstruct(graph, loop, blockId)) { + if (blockId == globalMerge || reached[blockId] || !isInsideLoopConstruct(graph, loop, blockId)) { continue; } - reached.push_back(blockId); + reached[blockId] = true; const auto& successors = graph.FindBlock(blockId).successors; pending.insert(pending.end(), successors.begin(), successors.end()); } + std::vector joins(graph.blocks.size(), false); std::vector joining = {globalMerge}; + joins[globalMerge] = true; for (std::size_t index = 0; index < joining.size(); ++index) { for (const auto predecessor : graph.FindBlock(joining[index]).predecessors) { - if (contains(reached, predecessor) && !contains(joining, predecessor)) { + if (reached[predecessor] && !joins[predecessor]) { + joins[predecessor] = true; joining.push_back(predecessor); } } } sortUnique(joining); - if (!contains(joining, trueTarget) || !contains(joining, falseTarget)) { + if (!joins[trueTarget] || !joins[falseTarget]) { return globalMerge; } std::vector> postDominators(graph.blocks.size()); for (const auto blockId : joining) { postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; } + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto blockId : joining) { - if (blockId == globalMerge) { + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) { continue; } auto next = joining; for (const auto successor : graph.FindBlock(blockId).successors) { - if (contains(joining, successor)) { + if (joins[successor]) { next = intersectSorted(next, postDominators[successor]); } } @@ -433,9 +460,11 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock postDominators[block.id] = {block.id}; } } + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto& block : graph.blocks) { + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; if (kept[block.id].empty()) { continue; } @@ -443,8 +472,7 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock for (std::size_t i = 1; i < kept[block.id].size(); ++i) { intersectAnalysisSet(next, postDominators[kept[block.id][i]]); } - addUnique(next, block.id); - sortUnique(next); + addSortedUnique(next, block.id); if (next != postDominators[block.id]) { replaceAnalysisSet(postDominators[block.id], std::move(next)); changed = true; @@ -506,9 +534,11 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur } std::vector> postDominators(count, allBlockIds(count)); postDominators[join] = {join}; + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto& block : graph.blocks) { + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; if (!joins[block.id] || block.id == join) { continue; } @@ -810,6 +840,8 @@ constexpr std::uint32_t CloneWordLimit = 1024u; constexpr std::uint32_t CloneWordFloor = 256u; constexpr std::uint32_t CloneShareDivisor = 8u; constexpr std::uint32_t CloneBudgetDivisor = 4u; +constexpr std::uint32_t SplitGrowthFactor = 16u; +constexpr std::uint32_t SplitGrowthFloor = 64u; struct SplitBudget { std::uint32_t programWords = 0; @@ -1974,10 +2006,12 @@ void Structurizer::computeDominatorTree(ControlFlowGraph& graph) const { block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; } + const auto order = analysisOrder(graph, false); bool changed = true; while (changed) { changed = false; - for (auto& block : graph.blocks) { + for (const auto id : order) { + auto& block = graph.blocks[id]; if (block.id == graph.entryBlock) { continue; } @@ -2044,19 +2078,7 @@ void Structurizer::computePostDominators(ControlFlowGraph& graph) const { block.postDominators = block.successors.empty() ? std::vector{block.id} : all; } - std::vector order; - order.reserve(count); - std::vector ordered(count, false); - if (graph.entryBlock < count) { - const auto forward = reversePostOrder(graph); - for (auto it = forward.rbegin(); it != forward.rend(); ++it) { - order.push_back(*it); - ordered[*it] = true; - } - } - for (std::uint32_t id = 0; id < count; ++id) { - if (!ordered[id]) order.push_back(id); - } + const auto order = analysisOrder(graph, true); bool changed = true; while (changed) { @@ -2207,12 +2229,16 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); + const auto growthBudget = std::max(SplitGrowthFloor, originalBlockCount * SplitGrowthFactor); const auto programWords = estimatedSpirvWords(graph, allBlockIds(originalBlockCount)); SplitBudget budget{programWords, std::max(CloneWordLimit, programWords / CloneBudgetDivisor), std::max(CloneWordLimit, programWords), nextGotoVariable(graph)}; for (std::uint32_t splits = 0; splits < splitBudget; ++splits) { if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits, routeSharedExits) && !splitOneReturnJoin(graph, relaxReturnJoins)) { return; } + if (graph.blocks.size() > growthBudget) { + throw std::runtime_error("CFG shared merge splitting exceeded growth budget: originalBlocks=" + std::to_string(originalBlockCount) + " currentBlocks=" + std::to_string(graph.blocks.size()) + " growthBudget=" + std::to_string(growthBudget)); + } rebuildPredecessors(graph); recomputeAnalyses(graph); verifyReducibility(graph); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 205a66251c..35f33304f8 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -114,4 +114,4 @@ - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. -- [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first, so a graph on which they spend seconds before refusing is not prepared faster. `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests +- [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests From 07d4b36c506e4f565324e320ada597d7551b22af Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 08:44:10 +0200 Subject: [PATCH 209/353] test(recompiler): cover slow shared merge splitting and its growth budget Structurize the 20-block graph whose merge splitting adds one forwarder per round until its round budget runs out, and a 12-block loop whose 2000-word latch is too large to clone, so merge splitting keeps adding junctions. Both must give structured branches, every original block once and the same executions as the input. Give the loop an unsupported exit, which keeps the reach-flag fallback out, and require Structurize to report the growth budget refusal instead of the round budget refusal it reached before after 676 blocks. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/Structurizer.cpp | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index 6177ad842f..ca8b02d465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -238,8 +238,9 @@ void requireUnconditionalEntry(const ControlFlowGraph& original, const ControlFl throw std::runtime_error(std::string(name) + ": the entry block does not run unconditionally first"); } -void requireSameStructuredExecutions(const std::vector>& successors, const char* name) { - const auto original = makeGraph(successors); +void requireSameStructuredExecutions(const std::vector>& successors, const char* name, const std::map& words = {}) { + auto original = makeGraph(successors); + for (const auto& [blockId, count] : words) original.blocks[blockId].estimatedSpirvWords = count; auto graph = original; Structurizer{}.Structurize(graph); requireExactPostDominators(graph, name); @@ -250,6 +251,17 @@ void requireSameStructuredExecutions(const std::vector> originalPostDominators(const ControlFlowGraph& graph) { std::vector all; for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); @@ -440,6 +452,13 @@ int main() { requireSameStructuredExecutions({{1, 2}, {2, 0}, {1}}, "a cycle entered at both blocks inside a loop that never ends"); requireSameStructuredExecutions({{1, 3}, {2}, {1}, {}}, "a branch that returns or enters a loop that never ends"); requireSameStructuredExecutions({{1}, {1}}, "a block that branches to itself forever"); + requireSameStructuredExecutions({{1, 13}, {2, 8}, {3, 6}, {4, 0}, {5, 4}, {6}, {}, {}, {5, 9}, {10, 0}, {11}, {11, 12}, {}, {14}, {15}, {16, 17}, {4}, {18, 19}, {7, 0}, {}}, "merge splitting that adds one forwarder per round until its budget runs out"); + const std::vector> junctionLoop{{1, 2}, {}, {3, 10}, {4, 8}, {6, 0}, {}, {7, 8}, {0}, {5, 9}, {7}, {9, 11}, {11}}; + requireSameStructuredExecutions(junctionLoop, "a loop whose latch is too large to clone, so merge splitting keeps adding junctions", {{7u, 2000u}}); + auto unsupportedExit = makeGraph(junctionLoop); + unsupportedExit.blocks[7].estimatedSpirvWords = 2000u; + unsupportedExit.blocks[5].terminator.kind = TerminatorKind::Unsupported; + requireGrowthBudgetRefusal(std::move(unsupportedExit), "a junction loop whose unsupported exit keeps the reach-flag fallback out"); requireOriginalDominanceSets(makeGraph(diamonds), "raw diamond chain"); requireOriginalDominanceSets(makeGraph({{1}, {2, 3}, {1}, {}}), "a loop with an exit"); requireOriginalDominanceSets(makeGraph({{1}, {2}, {1}}), "a non-exiting SCC"); From 72211b70fe40e1e8c91f8b18c52a159ed1228b16 Mon Sep 17 00:00:00 2001 From: Adelmo Junior Date: Mon, 12 Oct 2026 09:47:50 -0300 Subject: [PATCH 210/353] fix(kernel): honor the declared flexible memory budget Read the optional aligned budget from param.json and apply it consistently to configured, available and mapped flexible memory. Add parser and allocation regression coverage. --- .../AppMetadata/include/ParamJsonParser.hpp | 2 + .../AppMetadata/src/ParamJsonParser.cpp | 15 ++++ core/libs/prx/libkernel/CMakeLists.txt | 29 ++++++ .../prx/libkernel/DirectMemory/Export.cpp | 24 +++-- core/libs/tests/GuestFlexibleMemoryBudget.cpp | 89 +++++++++++++++++++ core/libs/tests/GuestFlexibleMemoryModule.cpp | 3 + core/libs/tests/ParamJsonParser.cpp | 23 +++++ 7 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestFlexibleMemoryBudget.cpp create mode 100644 core/libs/tests/GuestFlexibleMemoryModule.cpp diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index be912c6e78..22f615c522 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -3,6 +3,7 @@ #include #include +#include #include struct ParsedParamJson { @@ -10,6 +11,7 @@ struct ParsedParamJson { std::string titleId; std::uint64_t downloadDataSizeMiB = 0; std::uint32_t attribute3 = 0; + std::optional flexibleMemorySizeBytes; std::int32_t userDefinedParams[4] = {}; }; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 585b732a1d..2652ae8b87 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include @@ -314,6 +315,20 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { const auto parsed = std::from_chars(number.data(), number.data() + number.size(), result.attribute3); if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json attribute3 exceeds uint32 range"); } + if (const JsonValue* kernel = findObjectMember(root, "kernel")) { + if (const JsonValue* flexible = findObjectMember(*kernel, "flexibleMemorySize")) { + const auto& number = flexible->numberText; + if (flexible->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) + throw std::runtime_error("param.json kernel.flexibleMemorySize is not an unsigned integer"); + std::uint64_t bytes = 0; + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), bytes); + if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds uint64 range"); + if (bytes == 0 || (bytes & (PS5_PAGE_SIZE - 1)) != 0) + throw std::runtime_error("param.json kernel.flexibleMemorySize must be a positive multiple of 16 KiB"); + result.flexibleMemorySizeBytes = bytes; + } + } if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); if (downloadData->numberText.find_first_of("-.eE") == std::string::npos) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 561975af14..674b626e80 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1155,6 +1155,35 @@ if(BUILD_TESTING) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_flexible_memory_budget_tests ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryBudget.cpp) + target_include_directories(guest_flexible_memory_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_flexible_memory_budget_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_flexible_memory_budget_tests) + add_library(guest_flexible_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryModule.cpp) + set_target_properties(guest_flexible_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_flexible_memory_budget_tests guest_flexible_memory_module) + target_compile_definitions(guest_flexible_memory_budget_tests PRIVATE GUEST_FLEXIBLE_MEMORY_MODULE="$") + foreach(flexibleMemoryCase IN ITEMS declared:838860800 limited:16777216 none:0 missing:0 invalid:true) + string(REPLACE ":" ";" flexibleMemoryCase ${flexibleMemoryCase}) + list(GET flexibleMemoryCase 0 flexibleMemoryName) + list(GET flexibleMemoryCase 1 flexibleMemorySize) + set(flexibleMemoryRun ${CMAKE_CURRENT_BINARY_DIR}/guest_flexible_memory_${flexibleMemoryName}) + file(MAKE_DIRECTORY ${flexibleMemoryRun}) + if(NOT flexibleMemoryName STREQUAL "missing") + set(flexibleMemoryField "") + if(NOT flexibleMemoryName STREQUAL "none") + set(flexibleMemoryField ",\"kernel\":{\"flexibleMemorySize\":${flexibleMemorySize}}") + endif() + file(WRITE ${flexibleMemoryRun}/app0/sce_sys/param.json + "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}${flexibleMemoryField}}") + endif() + add_test(NAME guest_flexible_memory_${flexibleMemoryName} COMMAND guest_flexible_memory_budget_tests ${flexibleMemoryName} + WORKING_DIRECTORY ${flexibleMemoryRun}) + if(WIN32) + set_tests_properties(guest_flexible_memory_${flexibleMemoryName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_mlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestMlock.cpp) target_include_directories(guest_mlock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mlock_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index ea7e00b055..e8baaf3781 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -36,7 +37,18 @@ extern "C" int* APS5_VABI __error_nid_postfix(); namespace { -constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; +size_t _configuredFlexibleSize() { + static const size_t size = [] { + constexpr std::uint64_t defaultSize = 448ULL * 1024 * 1024; + const auto path = ResolvePath_nid_no_patch("/app0/sce_sys/param.json"); + const auto bytes = std::filesystem::exists(path) ? parseParamJson(path).flexibleMemorySizeBytes.value_or(defaultSize) : defaultSize; + if (bytes > std::numeric_limits::max()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds host size range"); + std::fprintf(stderr, "[memory] configured flexible memory: 0x%llx bytes (%llu MiB)\n", static_cast(bytes), static_cast(bytes >> 20)); + return static_cast(bytes); + }(); + return size; +} constexpr int PRT_APERTURE_COUNT = 3; @@ -94,11 +106,12 @@ size_t _flexibleUsedLocked() { } int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t configured = _configuredFlexibleSize(); const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); const size_t used = images + _flexibleUsedLocked(); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, used)) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, static_cast(FLEXIBLE_MEMORY_SIZE)); + if (len > configured - std::min(configured, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); return SCE_KERNEL_ERROR_ENOMEM; } const int result = DoMapAnon(addr, len, prot, flags); @@ -360,15 +373,16 @@ int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; + const size_t configured = _configuredFlexibleSize(); const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, images + _flexibleUsedLocked()); + *size = configured - std::min(configured, images + _flexibleUsedLocked()); return 0; } int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; - *size = FLEXIBLE_MEMORY_SIZE; + *size = _configuredFlexibleSize(); return 0; } diff --git a/core/libs/tests/GuestFlexibleMemoryBudget.cpp b/core/libs/tests/GuestFlexibleMemoryBudget.cpp new file mode 100644 index 0000000000..7938cd2a8c --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryBudget.cpp @@ -0,0 +1,89 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelConfiguredFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); +std::uint64_t GuestImageSegmentBytes_nid_no_patch(); +void* APS5_VABI dlopen_nid_postfix(const char*, int); +char* APS5_VABI dlerror_nid_postfix(); +int APS5_VABI dlclose_nid_postfix(void*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Flexible memory budget check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + const std::string_view mode = argv[1]; + Require(sceKernelConfiguredFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAvailableFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::size_t configured = 0; + if (mode == "invalid") { + bool rejected = false; + try { sceKernelConfiguredFlexibleMemorySize(&configured); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected && configured == 0); + return 0; + } + const std::size_t expected = (mode == "declared" ? 800u : mode == "limited" ? 16u : 448u) * 1024u * 1024u; + Require(sceKernelConfiguredFlexibleMemorySize(&configured) == 0 && configured == expected); + std::size_t before = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured); + void* module = nullptr; + if (mode == "declared") { + const std::filesystem::path fixture = GUEST_FLEXIBLE_MEMORY_MODULE; + AddPathAlias_nid_no_patch("budget_module", fixture.parent_path().string().c_str()); + const auto path = "/budget_module/" + fixture.filename().generic_string(); + module = dlopen_nid_postfix(path.c_str(), 2); + if (!module) std::fprintf(stderr, "Cannot load budget fixture: %s\n", dlerror_nid_postfix()); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(GuestImageSegmentBytes_nid_no_patch() > 448u * 1024u * 1024u); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured && before > 0x80000); + } + constexpr std::size_t length = 0x80000; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 7; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelReleaseFlexibleMemory(mapped, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length / 2); + Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + length / 2, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + void* oversized = nullptr; + Require(sceKernelMapFlexibleMemory(&oversized, before + PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && oversized == nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + if (mode == "limited") { + const auto length = before & ~(PS5_PAGE_SIZE - 1); + void* full = nullptr; + Require(sceKernelMapFlexibleMemory(&full, length, 3, 0) == 0); + void* extra = nullptr; + Require(sceKernelMapFlexibleMemory(&extra, PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && extra == nullptr); + Require(sceKernelReleaseFlexibleMemory(full, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + } + if (module) { + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + Require(available > before && available + GuestImageSegmentBytes_nid_no_patch() == configured); + } + std::puts("Flexible memory budget checks passed"); +} diff --git a/core/libs/tests/GuestFlexibleMemoryModule.cpp b/core/libs/tests/GuestFlexibleMemoryModule.cpp new file mode 100644 index 0000000000..1bf220a810 --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryModule.cpp @@ -0,0 +1,3 @@ +extern "C" { +unsigned char guestFlexibleMemoryModuleData[512u * 1024u * 1024u] = {}; +} diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index dd24b91013..719a0852ff 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -98,6 +98,28 @@ static void CheckNesting(const std::filesystem::path& path) { } } +static void CheckFlexibleMemorySize(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; + Require(!ParseText(path, prefix + "}").flexibleMemorySizeBytes, "Absent kernel changed the default budget"); + Require(!ParseText(path, prefix + R"(,"kernel":{}})").flexibleMemorySizeBytes, "Absent flexible size changed the default budget"); + for (const auto bytes : {std::uint64_t{16384}, std::uint64_t{838860800}, std::uint64_t{18446744073709535232ull}}) { + const auto parsed = ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + std::to_string(bytes) + "}}"); + Require(parsed.flexibleMemorySizeBytes == bytes, "Incorrect flexible memory size in bytes"); + } + for (const auto* value : {"0", "-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + value + "}}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, std::string("Accepted invalid flexible memory size: ") + value); + } + for (const auto* value : {"null", "true", "[]", "1", "\"kernel\""}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":)" + value + "}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "Accepted a non-object kernel"); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -181,6 +203,7 @@ int main() { try { CheckNumericLocale(path); CheckUtf8(path); + CheckFlexibleMemorySize(path); CheckNumberSyntax(path); const std::pair sizes[] = { {"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096}, From 12851ef037bafb647808789216c79821d0959739 Mon Sep 17 00:00:00 2001 From: Adelmo Junior Date: Tue, 13 Oct 2026 09:10:45 -0300 Subject: [PATCH 211/353] docs: document flexible memory budget behavior --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2c6dd4e1bc..2ab883c493 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -37,7 +37,7 @@ - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified -- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the 448 MiB flexible budget while they are loaded, for this size and for the flexible maps, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165) maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [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 From 99eaf73102ce5f9d4c530720ff76ad93a81bcdce Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Tue, 13 Oct 2026 22:11:07 +0300 Subject: [PATCH 212/353] perf(libkernel): wake guest mutex waiters on release on Windows Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 7 + core/libs/prx/libkernel/Pthread/src/Cond.cpp | 19 +- core/libs/prx/libkernel/Pthread/src/Mutex.cpp | 37 +++- core/libs/prx/libkernel/Time/TimedWait.cpp | 120 +++++++++-- .../prx/libkernel/Time/include/TimedWait.hpp | 66 +++--- core/libs/tests/GuestMutexContention.cpp | 200 ++++++++++++++++++ 6 files changed, 395 insertions(+), 54 deletions(-) create mode 100644 core/libs/tests/GuestMutexContention.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 72ce92342a..1d6c321f68 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1255,6 +1255,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) + target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_mutex_contention_tests) + add_test(NAME guest_mutex_contention COMMAND guest_mutex_contention_tests) + set_tests_properties(guest_mutex_contention PROPERTIES TIMEOUT 20 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadRename.cpp) target_include_directories(guest_pthread_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 4cddf4d5b8..d576c64ff0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -60,6 +60,19 @@ int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { return 0; } +template +struct WakingLock { + PthreadMutex mutex; + TNative& native; + void lock() { native.lock(); } + void unlock() { + native.unlock(); +#ifdef _WIN32 + TimedWait::Unpark(mutex); +#endif + } +}; + int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { auto* c = resolveCond(cond); PthreadMutex m = nullptr; @@ -72,7 +85,8 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()), timedOut); } } trace{caller, timedOut, waitStart}; if (m->_type == MutexType::Recursive) { - std::unique_lock lock(m->_rmtx, std::adopt_lock); + WakingLock waking{m, m->_rmtx}; + std::unique_lock lock(waking, std::adopt_lock); const auto previousCount = m->_count; m->_count = 0; m->_owner.store(std::thread::id{}, std::memory_order_release); @@ -87,7 +101,8 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional lock(m->_mtx, std::adopt_lock); + 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); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 4cf22db6ef..93e0e693b6 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -2,6 +2,7 @@ #include "../include/Mutex.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -87,14 +88,18 @@ int MutexOperations::Timedlock(PthreadMutex* mutex, const KernelTimespec* abstim if (std::chrono::duration(duration) >= std::chrono::duration(std::chrono::system_clock::duration::max())) throw std::overflow_error("Absolute mutex timeout exceeds the host clock range"); const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast(duration)); - return acquireMutex(resolveMutex(mutex, true), [&](auto& native) { + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [&](auto& native) { #ifdef _WIN32 - while (!native.try_lock()) { - if (std::chrono::system_clock::now() >= deadline) return false; - TimedWait::SleepNanos(1000000); + for (;;) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) return native.try_lock(); + const auto remaining = std::chrono::duration_cast(deadline - now).count(); + const std::uint64_t slice = TimedWait::NowNanos() + static_cast(std::min(remaining, 100000000)); + if (TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, &slice)) return true; } - return true; #else + static_cast(current); return native.try_lock_until(deadline); #endif }, sceTimedOut, false); @@ -169,13 +174,15 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { } int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { - return acquireMutex(resolveMutex(mutex, true), [](auto& native) { + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [current](auto& native) { #ifdef _WIN32 - while (!native.try_lock()) TimedWait::SleepNanos(1000000); + return TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, nullptr); #else + static_cast(current); native.lock(); -#endif return true; +#endif }, 0, false); } @@ -183,21 +190,29 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { auto* current = resolveMutex(mutex, false); if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) return sceNotPermitted; + bool released = true; if (current->_type == MutexType::Recursive) { - if (--current->_count == 0) + released = --current->_count == 0; + if (released) current->_owner.store(std::thread::id{}, std::memory_order_release); current->_rmtx.unlock(); } else { current->_owner.store(std::thread::id{}, std::memory_order_release); current->_mtx.unlock(); } +#ifdef _WIN32 + if (released) TimedWait::Unpark(current); +#else + static_cast(released); +#endif return 0; } int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { const auto deadline = TimedWait::DeadlineNanos(usec); - return acquireMutex(resolveMutex(mutex, true), [=](auto& native) { - return TimedWait::AcquireUntil(deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [=](auto& native) { + return TimedWait::AcquireUntil(current, deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); }, sceTimedOut, false); } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 6d57117e4e..8535253896 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -3,6 +3,7 @@ #include #include +#include #ifdef _WIN32 #include @@ -108,10 +109,11 @@ void SleepNanosCoarse(std::uint64_t nanos) { struct Waiter { HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr); HANDLE timer = CreateHighResolutionTimer(); - HANDLE sleepTimers[3]{}; + HANDLE sleepTimers[2]{}; Waiter* previous = nullptr; Waiter* next = nullptr; Waiter* nested = nullptr; + const void* key = nullptr; bool queued = false; bool busy = false; @@ -239,6 +241,111 @@ void Condition::NotifyAll() { } } +namespace { + +struct alignas(64) ParkBucket { + std::mutex lock; + Waiter* head = nullptr; + Waiter* tail = nullptr; + std::atomic parked{0}; +}; + +ParkBucket parkBuckets[256]; + +ParkBucket& ParkBucketFor(const void* key) { + return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; +} + +struct DeliveryDeferral { + DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } + ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } +}; + +void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { + if (waiter->previous) waiter->previous->next = waiter->next; + else bucket.head = waiter->next; + if (waiter->next) waiter->next->previous = waiter->previous; + else bucket.tail = waiter->previous; + waiter->previous = nullptr; + waiter->next = nullptr; + waiter->queued = false; +} + +bool LeavePark(ParkBucket& bucket, Waiter* waiter) { + bool signaled; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + signaled = !waiter->queued; + if (!signaled) UnlinkParked(bucket, waiter); + } + if (signaled) WaitForSingleObject(waiter->event, 0); + return signaled; +} + +void ReleaseParked(ParkBucket& bucket, Waiter* waiter) { + bucket.parked.fetch_sub(1, std::memory_order_seq_cst); + waiter->busy = false; + DrainApcs(); +} + +} // namespace + +void SpinPause() { + YieldProcessor(); +} + +Waiter* BeginPark(const void* key) { + auto& bucket = ParkBucketFor(key); + Waiter* waiter; + { + DeliveryDeferral deferral; + waiter = AcquireWaiter(); + waiter->key = key; + bucket.parked.fetch_add(1, std::memory_order_seq_cst); + std::lock_guard lock(bucket.lock); + waiter->queued = true; + waiter->next = nullptr; + waiter->previous = bucket.tail; + if (bucket.tail) bucket.tail->next = waiter; + else bucket.head = waiter; + bucket.tail = waiter; + } + DrainApcs(); + return waiter; +} + +bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos) { + auto& bucket = ParkBucketFor(key); + bool signaled = deadlineNanos ? WaitEventUntil(waiter, *deadlineNanos) : AlertableWait(1, &waiter->event, INFINITE) == WAIT_OBJECT_0; + if (!signaled) signaled = LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); + if (!signaled && !deadlineNanos) throw std::runtime_error("TimedWait: an untimed parked wait ended without a wake"); + return signaled; +} + +void CancelPark(const void* key, Waiter* waiter) { + auto& bucket = ParkBucketFor(key); + LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); +} + +void Unpark(const void* key) { + auto& bucket = ParkBucketFor(key); + if (bucket.parked.load(std::memory_order_seq_cst) == 0) return; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + for (Waiter* waiter = bucket.head; waiter; waiter = waiter->next) { + if (waiter->key != key) continue; + UnlinkParked(bucket, waiter); + SetEvent(waiter->event); + break; + } + } + DrainApcs(); +} + #else void Condition::NotifyOne() { @@ -312,17 +419,6 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } -void PollSleepUntil(std::uint64_t deadlineNanos) { -#ifdef _WIN32 - const std::uint64_t now = NowNanos(); - if (deadlineNanos <= now) return; - const HANDLE timer = ThreadTimer(2); - if (!TimerWait(timer, deadlineNanos - now)) AlertableSleep(WholeMilliseconds(deadlineNanos - now)); -#else - SleepUntil(deadlineNanos); -#endif -} - void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 887ff0ebb8..4f7ccc32b5 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,38 +16,46 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); -void PollSleepUntil(std::uint64_t deadlineNanos); - -constexpr std::uint64_t LOCK_BULK_NANOS = 40000000ULL; -constexpr std::uint64_t LOCK_POLL_NANOS = 500000ULL; -constexpr std::uint64_t LOCK_TAIL_NANOS = 1000000ULL; - -template -bool AcquireUntil(std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { - if (Coarse()) return tryLockFor(RemainingMicros(deadlineNanos)); - for (;;) { - if (tryLock()) return true; - const std::uint64_t now = NowNanos(); - if (now >= deadlineNanos) return false; - const std::uint64_t remaining = deadlineNanos - now; -#ifdef _WIN32 - if (remaining > LOCK_TAIL_NANOS) { - PollSleepUntil(now + (remaining < LOCK_POLL_NANOS ? remaining : LOCK_POLL_NANOS)); - continue; - } -#else - if (remaining > LOCK_BULK_NANOS) { - if (tryLockFor((remaining - LOCK_BULK_NANOS) / 1000ULL)) return true; - continue; - } -#endif - if (remaining <= LOCK_TAIL_NANOS) SleepUntil(deadlineNanos); - else PollSleepUntil(now + LOCK_POLL_NANOS); - } -} struct Waiter; +#ifdef _WIN32 +constexpr unsigned PARK_SPINS = 1000; + +void SpinPause(); +Waiter* BeginPark(const void* key); +bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); +void CancelPark(const void* key, Waiter* waiter); +void Unpark(const void* key); + +template +bool ParkAcquire(const void* key, TTryLock tryLock, const std::uint64_t* deadlineNanos) { + for (unsigned spin = 0; spin < PARK_SPINS; ++spin) { + if (tryLock()) return true; + SpinPause(); + } + for (;;) { + Waiter* waiter = BeginPark(key); + if (tryLock()) { + CancelPark(key, waiter); + return true; + } + if (!FinishPark(key, waiter, deadlineNanos)) return tryLock(); + if (tryLock()) return true; + } +} +#endif + +template +bool AcquireUntil(const void* key, std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { +#ifdef _WIN32 + if (!Coarse()) return ParkAcquire(key, tryLock, &deadlineNanos); +#endif + static_cast(key); + static_cast(tryLock); + return tryLockFor(RemainingMicros(deadlineNanos)); +} + class Condition { public: Condition() = default; diff --git a/core/libs/tests/GuestMutexContention.cpp b/core/libs/tests/GuestMutexContention.cpp new file mode 100644 index 0000000000..dbb0da5e2f --- /dev/null +++ b/core/libs/tests/GuestMutexContention.cpp @@ -0,0 +1,200 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr*); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr*, int); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr*); +int APS5_VABI scePthreadMutexInit(PthreadMutex*, const PthreadMutexattr*, const char*); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex*); +int APS5_VABI scePthreadMutexLock(PthreadMutex*); +int APS5_VABI scePthreadMutexTimedlock(PthreadMutex*, KernelUseconds); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex*); +} + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr int TimedOut = static_cast(0x8002003cu); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +std::uint64_t ThreadCycles() { + ULONG64 cycles = 0; + Require(QueryThreadCycleTime(GetCurrentThread(), &cycles) != 0, "QueryThreadCycleTime failed"); + return cycles; +} + +std::uint64_t BusyCycles(int milliseconds) { + const auto before = ThreadCycles(); + const auto until = Clock::now() + std::chrono::milliseconds(milliseconds); + while (Clock::now() < until) YieldProcessor(); + return ThreadCycles() - before; +} + +struct Handoff { + PthreadMutex mutex = nullptr; + bool timed = false; + bool destroy = false; + std::atomic waiting{false}; + std::atomic released{0}; + std::int64_t latencyNanos = 0; + std::uint64_t waitCycles = 0; +}; + +struct HandoffResult { + std::int64_t latencyNanos; + std::uint64_t waitCycles; +}; + +void* APS5_VABI HandoffWorker(void* opaque) { + auto& context = *static_cast(opaque); + const auto cyclesBefore = ThreadCycles(); + context.waiting.store(true); + Require((context.timed ? scePthreadMutexTimedlock(&context.mutex, 5000000) : scePthreadMutexLock(&context.mutex)) == 0, "the waiter did not acquire the mutex"); + const auto acquired = Clock::now().time_since_epoch().count(); + context.waitCycles = ThreadCycles() - cyclesBefore; + context.latencyNanos = acquired - context.released.load(); + Require(scePthreadMutexUnlock(&context.mutex) == 0, "the waiter could not unlock"); + if (context.destroy) Require(scePthreadMutexDestroy(&context.mutex) == 0, "the waiter could not destroy the mutex"); + return nullptr; +} + +HandoffResult RunHandoff(PthreadMutex mutex, bool timed, bool destroy, int holdMilliseconds, int extraMicroseconds = 0) { + Handoff context; + context.mutex = mutex; + context.timed = timed; + context.destroy = destroy; + PthreadMutex holder = mutex; + Require(scePthreadMutexLock(&holder) == 0, "the holder could not lock"); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, HandoffWorker, &context, "mutex waiter") == 0, "cannot create the waiter"); + while (!context.waiting.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(holdMilliseconds)); + const auto extra = Clock::now() + std::chrono::microseconds(extraMicroseconds); + while (Clock::now() < extra) YieldProcessor(); + context.released.store(Clock::now().time_since_epoch().count()); + Require(scePthreadMutexUnlock(&holder) == 0, "the holder could not unlock"); + Require(scePthreadJoin(worker, nullptr) == 0, "cannot join the waiter"); + return {context.latencyNanos, context.waitCycles}; +} + +PthreadMutex CreateMutex(bool recursive) { + PthreadMutexattr attributes = nullptr; + Require(scePthreadMutexattrInit(&attributes) == 0 && scePthreadMutexattrSettype(&attributes, recursive ? 2 : 1) == 0, "cannot prepare mutex attributes"); + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &attributes, nullptr) == 0, "cannot create the mutex"); + Require(scePthreadMutexattrDestroy(&attributes) == 0, "cannot destroy mutex attributes"); + return mutex; +} + +struct Contention { + PthreadMutex mutex = nullptr; + std::uint64_t rounds = 0; + std::atomic start{false}; + std::atomic stop{false}; + std::uint64_t shared = 0; + std::array acquired{}; + std::atomic next{0}; +}; + +void* APS5_VABI ContentionWorker(void* opaque) { + auto& context = *static_cast(opaque); + const int index = context.next.fetch_add(1); + PthreadMutex mutex = context.mutex; + while (!context.start.load()) std::this_thread::yield(); + std::uint64_t count = 0; + while (context.rounds ? count < context.rounds : !context.stop.load(std::memory_order_relaxed)) { + Require(scePthreadMutexLock(&mutex) == 0, "contended lock failed"); + const std::uint64_t seen = context.shared; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + context.shared = seen + 1; + Require(scePthreadMutexUnlock(&mutex) == 0, "contended unlock failed"); + ++count; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + } + context.acquired[index] = count; + return nullptr; +} + +void* APS5_VABI TimeoutWorker(void* opaque) { + PthreadMutex mutex = *static_cast(opaque); + const auto started = Clock::now(); + Require(scePthreadMutexTimedlock(&mutex, 20000) == TimedOut, "a timed lock of a held mutex did not time out"); + Require(Clock::now() - started >= std::chrono::milliseconds(20), "a timed lock of a held mutex timed out before its deadline"); + return nullptr; +} + +} + +int main(int argc, char** argv) { + const bool report = argc > 1 && std::strcmp(argv[1], "--report") == 0; + const std::uint64_t busyCycles = report ? BusyCycles(20) * 10 : 0; + for (const bool recursive : {false, true}) { + PthreadMutex mutex = CreateMutex(recursive); + for (const bool timed : {false, true}) { + if (!report) { + for (int round = 0; round < 4; ++round) RunHandoff(mutex, timed, false, 3); + continue; + } + std::vector latencies; + for (int round = 0; round < 31; ++round) latencies.push_back(RunHandoff(mutex, timed, false, 3, round * 617 % 1000).latencyNanos); + std::sort(latencies.begin(), latencies.end()); + const auto blocked = RunHandoff(mutex, timed, false, 200); + const double share = 100.0 * static_cast(blocked.waitCycles) / static_cast(busyCycles); + std::printf("%s %s: handoff median %.1f us, p90 %.1f us, max %.1f us; a waiter blocked for 200 ms used %.1f %% of one processor\n", + recursive ? "recursive" : "errorcheck", timed ? "timed lock" : "lock", + latencies[15] / 1000.0, latencies[27] / 1000.0, latencies[30] / 1000.0, share); + } + PthreadMutex held = mutex; + Require(scePthreadMutexLock(&held) == 0, "cannot hold the mutex for the timeout"); + Pthread timeout = nullptr; + Require(scePthreadCreate(&timeout, nullptr, TimeoutWorker, &mutex, "mutex timeout") == 0, "cannot create the timeout waiter"); + Require(scePthreadJoin(timeout, nullptr) == 0, "cannot join the timeout waiter"); + Require(scePthreadMutexUnlock(&held) == 0, "cannot release the mutex after the timeout"); + Contention context; + context.mutex = mutex; + context.rounds = report ? 0 : 20000; + std::array workers{}; + for (auto& worker : workers) Require(scePthreadCreate(&worker, nullptr, ContentionWorker, &context, "mutex contender") == 0, "cannot create a contender"); + const auto started = Clock::now(); + context.start.store(true); + if (report) { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + context.stop.store(true); + } + for (auto& worker : workers) Require(scePthreadJoin(worker, nullptr) == 0, "cannot join a contender"); + const double seconds = std::chrono::duration(Clock::now() - started).count(); + std::uint64_t total = 0; + for (const auto count : context.acquired) total += count; + Require(total == context.shared, "the mutex did not exclude its contenders"); + const auto fewest = *std::min_element(context.acquired.begin(), context.acquired.end()); + if (report) { + std::printf("%s: four contenders made %.0f acquisitions per second, the slowest %.0f per second\n", + recursive ? "recursive" : "errorcheck", total / seconds, fewest / seconds); + } else { + Require(fewest == context.rounds, "a contender did not finish its acquisitions"); + } + Require(scePthreadMutexDestroy(&mutex) == 0, "cannot destroy the mutex"); + for (int round = 0; round < 200; ++round) RunHandoff(CreateMutex(recursive), round % 2 != 0, true, 0); + } + std::puts("guest mutex contention: PASS"); + return 0; +} From e015772307b216aa8b8f40072051f2d0a0ff2301 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 23:28:43 +0000 Subject: [PATCH 213/353] fix(shader): plan a table T# that several image operations share TryMakeTableImage planned a T# read from a bindless image table only when its eight scalar reads had no user other than that one GetImageResource. Descriptor-phi splitting gives each image operation its own copy of the T# over the same reads, so a table entry that is both queried and sampled was refused, and GetHandle then rejected the scalar reads. Accept reads whose every user is a GetImageResource built from exactly those eight reads; each copy is planned as the same table. Crimson Desert (PPSA25286) stops on such a shader at start-up: image_get_resinfo and image_sample_lz on one s_buffer_load_dwordx8 T#. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 17 +++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 15 +++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6e412161ac..3611aa3b7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -923,6 +923,23 @@ void verifyBindlessTable() { latch.context.memory = latchRegions; require(!Recompile(latch, *latchCapture)->spirv.empty(), "bindless: a sample in a loop latch did not compile"); + std::vector queryCode(wholeCode.begin(), wholeCode.begin() + 8); + queryCode.insert(queryCode.end(), {0xbea80380u, 0x7e040280u, 0x7e200500u, 0x80102810u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0380f08u, 0x00050402u, 0xf09c0f08u, 0x00450800u, 0x80288128u, 0xbf0a8228u, 0xbf85fff4u, 0xe0700000u, 0x80070800u, 0xe0700004u, 0x80070400u, 0xbf810000u}); + auto query = makeRequest(queryCode); + const auto queryPlan = GetResourcePlan(query); + require(std::ranges::count_if(queryPlan->descriptorSources, [](const DescriptorSource& source) { return source.indirectImage.has_value(); }) == 1, "bindless: a T# both queried and sampled was not planned as one table"); + AgcDriver::ShaderMemory queryMemory({}); + const auto queryCapture = queryMemory.Capture(query); + const auto queryDirect = static_cast(queryPlan->info.images.size()); + const auto queryRoot = tableRoot(*queryCapture, queryDirect); + require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); + query.context.memory = queryMemory.Regions(); + const auto queried = Recompile(query, *queryCapture); + require(!queried->spirv.empty(), "bindless: a T# both queried and sampled did not compile"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(queried->spirv, query.target.vulkanVersion, query.target.spirvVersion)); +#endif + // A table wider than the slots without a material pattern is rejected. fillSrt(100u); AgcDriver::ShaderMemory wideMemory({}); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 675f7d561f..7ad2880156 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -358,9 +358,17 @@ private: return true; } - static bool usesOnly(const IrValue& value, std::span users) { + static bool usedOnlyAsImage(const IrValue& value, const std::array& reads) { return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const IrValue* user) { - return std::ranges::find(users, user) != users.end(); + if (user->Opcode() != IrOpcode::GetImageResource || user->ArgumentCount() != reads.size()) { + return false; + } + for (std::uint32_t dword = 0; dword < reads.size(); dword++) { + if (user->Argument(dword)->Resolve() != reads[dword]) { + return false; + } + } + return true; }); } @@ -535,9 +543,8 @@ private: } entryOffset += immediateOffset; - const std::array imageUsers {&handle}; for (const auto* read : heapReads) { - if (!usesOnly(*read, imageUsers)) { + if (!usedOnlyAsImage(*read, heapReads)) { return false; } } From af2b86928122695ee4664890cfb9c4fa10a737d2 Mon Sep 17 00:00:00 2001 From: Sam <85789460+Sam-mon7@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:35:54 +0100 Subject: [PATCH 214/353] fix(shader): share delete access when reading cache entries on Windows WriteFileAtomically renames its handle over the published entry and closes it afterwards. A std::ifstream reader opened in that window fails with ERROR_SHARING_VIOLATION, and the cache reports the entry as gone. Read through one CreateFileW handle that shares read, write and delete. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 10 ++++++++ .../recompiler/ShaderCacheDirectory.hpp | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6ab1458817..6e672d77fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -540,6 +540,16 @@ void verifyAtomicReplacement(const std::filesystem::path& directory) { const std::vector original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}}; const std::vector replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}}; require(WriteFileAtomically(path, original), "cannot write the initial cache entry"); +#ifdef _WIN32 + { + const auto renaming = CreateFileW(path.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + require(renaming != INVALID_HANDLE_VALUE, "cannot retain the cache entry's rename handle"); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{renaming}; + std::vector bytes; + require(ReadWholeFile(path, bytes) && bytes == original, "cache reader failed while the published entry retained its rename handle"); + } +#endif { Reader old(path); require(old.Read() == original, "the retained reader did not read the original entry"); diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index db8e2f8fe1..ee9393e078 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -94,18 +94,42 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span& bytes) { bytes.clear(); try { +#ifdef _WIN32 + const auto file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return false; + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{file}; + LARGE_INTEGER fileSize{}; + if (!GetFileSizeEx(file, &fileSize)) return false; + const auto size = fileSize.QuadPart; +#else std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return false; const auto size = static_cast(file.tellg()); +#endif if (size < 0 || static_cast(size) > bytes.max_size() || static_cast(size) > static_cast(std::numeric_limits::max())) return false; +#ifdef _WIN32 + bytes.resize(static_cast(size)); + for (std::size_t offset = 0; offset < bytes.size();) { + const auto left = bytes.size() - offset; + const auto chunk = static_cast(left > MAXDWORD ? MAXDWORD : left); + DWORD count = 0; + if (!ReadFile(file, bytes.data() + offset, chunk, &count, nullptr) || count == 0) { + bytes.clear(); + return false; + } + offset += count; + } +#else if (!file.seekg(0, std::ios::beg)) return false; bytes.resize(static_cast(size)); if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { bytes.clear(); return false; } +#endif return true; } catch (const std::bad_alloc&) { bytes.clear(); From cd1757e03659f4a363e1b73a2d8a3711808e084b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 23:42:15 +0000 Subject: [PATCH 215/353] test(shader): check that a queried and sampled table T# is one image Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3611aa3b7d..8c19aab8b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -931,6 +931,7 @@ void verifyBindlessTable() { AgcDriver::ShaderMemory queryMemory({}); const auto queryCapture = queryMemory.Capture(query); const auto queryDirect = static_cast(queryPlan->info.images.size()); + require(queryDirect == 1u, "bindless: the queried and sampled T# became more than one image"); const auto queryRoot = tableRoot(*queryCapture, queryDirect); require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); query.context.memory = queryMemory.Regions(); From 945b97094c9a06e9f94063727fb93ff7bba03525 Mon Sep 17 00:00:00 2001 From: MidnightApora Date: Tue, 13 Oct 2026 18:44:22 -0500 Subject: [PATCH 216/353] fix(agc): accept 2D-array image loads and stores on 3D T#s A 2D-array image_load or image_store bound to a 3D T# addresses the slice as z. Add a 3D runtime mode for these accesses and drop loads and stores whose slice is past the volume depth. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 23 +++ .../tests/execution/ImageAddressDimension.cpp | 21 +++ .../ImageStoreArrayThroughVolume.cpp | 173 ++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 4 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 31 +++- 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6e412161ac..aa76abd67f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3522,6 +3522,28 @@ void verifyCubeDepthCompareMode() { require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); } +void verifyVolumeArrayLoadMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x02466600u, 0x01400000u, 0u, 0xa0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2DArray; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].dimension == RdnaImageDimension::Dim3D && !modes[selected].cube, "2D-array image load on a 3D T# did not select a volume runtime mode"); + image.fmaskCompatible = false; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "incompatible with the static runtime image interface", "2D-array image sample on a 3D T# was accepted"); + ImageResource store{}; + store.resourceClass = ImageResourceClass::Storage; + store.dimension = RdnaImageDimension::Dim2DArray; + store.written = true; + store.fmaskCompatible = false; + const auto storeModes = ResourceMaterializer::RuntimeImageModes(store); + require(storeModes[ResourceMaterializer::RuntimeImageMode(store, descriptor, storeModes)].dimension == RdnaImageDimension::Dim3D, "2D-array image store on a 3D T# did not select a volume runtime mode"); + store.atomic = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -3598,6 +3620,7 @@ int main(int argc, char** argv) { verifySrtReadPatching(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); + verifyVolumeArrayLoadMode(); verifySrgbDepthCompareModes(); verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp index b2ef1fc6ed..26b6bec54b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp @@ -53,6 +53,12 @@ alignas(256) constexpr std::array VolumeCode{ 0xbf810000, }; +alignas(256) constexpr std::array VolumeArrayCode{ + 0x34020087, 0xe030104c, 0x80001301, 0xe0301050, 0x80001401, 0xe0301054, 0x80001501, 0xbf8c3f70, + 0xf0001f28, 0x00010c13, 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, + 0xe070107c, 0x80000f01, 0xbf810000, +}; + alignas(256) constexpr std::array NarrowCode{ 0x34020087, 0xe0301040, 0x80001001, 0xe0301048, 0x80001101, 0xbf8c3f70, 0xf0900f00, 0x00610c10, 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, 0xbf810000, @@ -216,6 +222,20 @@ void CheckVolume() { } } +void CheckVolumeArray(AgcDriver::VulkanDevice& device) { + FillInput(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Buffer[tid * Words + PlainInput + 5u] = tid % Depth; + Run(device, VolumeArrayCode, VolumeDescriptor()); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{VolumeSliceMarker(tid % Depth), sample.x >> sample.level, sample.y >> sample.level, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + PlainResult + 4u + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_load 2d_array on a 3D texture: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + void CheckNarrow(AgcDriver::VulkanDevice& device) { FillInput(); Run(device, NarrowCode, TextureDescriptor(Texels.data())); @@ -260,6 +280,7 @@ int main() { FillVolume(); Run(*device, VolumeCode, VolumeDescriptor()); CheckVolume(); + CheckVolumeArray(*device); CheckNarrow(*device); CheckNarrowBilinearClamp(*device); std::puts("image address dimension tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp new file mode 100644 index 0000000000..b410321d5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201128, 0x00011714, 0x7e2c0282, 0xf0201128, + 0x00011714, 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, "image store array through volume: 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::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +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 VolumeDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = VolumeDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + 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(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 2d_array through a 3D T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 2d_array through a 3D T#: a slice past the volume changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store array through volume: the volume does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store array through volume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ce23f6fb6c..8695bdc450 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -643,7 +643,9 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2D && image.fmaskCompatible && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; + const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + if ((volumeLoad || volumeStore) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 93902d26d5..b56eb7a847 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -243,6 +244,24 @@ std::uint32_t LodU32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) return AddressU32(ctx, access, component); } +bool VolumeSliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; +} + +std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { + auto& state = ctx.state; + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto size = state.module.AllocateId(); + if (lod.has_value()) { + state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 3), size, image, *lod); + } else { + state.module.AddFunction(spv::OpImageQuerySize, TypeU32Vector(state, 3), size, image); + } + const auto depth = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), depth, size, 2u); + return Binary(state, spv::OpULessThan, TypeBool(state), AddressU32(ctx, access, 2u), depth); +} + std::uint32_t DrefValueF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const ImageSampleLayout& layout) { if (layout.dref == NoImageComponent || access.mem.imageAddressComponents <= layout.dref) { ctx.Fail(access.inst, "has no depth reference in the image address"); @@ -964,6 +983,9 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { if (access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2D) { condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, Binary(state, spv::OpIEqual, TypeBool(state), AddressU32(ctx, access, 2u), ConstantU32(state, 0u))); } + if (VolumeSliceAddress(access)) { + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, LoadSampledImageDescriptor(state, access.mem.resource, access.slot), LodU32(ctx, access))); + } ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { const auto descriptor = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); const auto color = state.module.AllocateId(); @@ -997,7 +1019,14 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Fail(access.inst, "stores through a bindless image table are unsupported"); } const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; - EmitIfCondition(state, ctx.Arg(access.inst, 3), [&]() { + auto condition = ctx.Arg(access.inst, 3); + if (VolumeSliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + const auto image = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); + } + EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); From 75c25e16f027d7bf8e3e5658434f0409df943374 Mon Sep 17 00:00:00 2001 From: David-cmrg <202826622+David-cmrg@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:42:10 +0200 Subject: [PATCH 217/353] fix(tests): use position-independent unwind data in the C++ error fixture --- core/libs/tests/GuestAmprCppError.S | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestAmprCppError.S b/core/libs/tests/GuestAmprCppError.S index 024758cd41..f5e0769766 100644 --- a/core/libs/tests/GuestAmprCppError.S +++ b/core/libs/tests/GuestAmprCppError.S @@ -3,8 +3,13 @@ .globl GuestCatch GuestCatch: .cfi_startproc +#ifdef _WIN32 .cfi_personality 0x00, __gxx_personality_v0_nid_postfix .cfi_lsda 0x00, .Lcatch_lsda +#else + .cfi_personality 0x9b, .Lcatch_personality + .cfi_lsda 0x1b, .Lcatch_lsda +#endif pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 @@ -34,10 +39,18 @@ GuestCatch: #ifdef _WIN32 .section .rdata,"dr" #else +.section .data.rel.ro,"aw",@progbits +.p2align 3 +.Lcatch_personality: + .quad __gxx_personality_v0_nid_postfix .section .rodata #endif .Lcatch_lsda: +#ifdef _WIN32 .byte 0xff, 0x80 +#else + .byte 0xff, 0x9b +#endif .uleb128 .Lcatch_types_end-.Lcatch_types_offset .Lcatch_types_offset: .byte 0x01 @@ -49,8 +62,13 @@ GuestCatch: .uleb128 1 .Lcatch_calls_end: .byte 1, 1, 2, 0 +#ifdef _WIN32 .quad FixtureCatchType .quad FixtureOtherType +#else + .long FixtureCatchType-. + .long FixtureOtherType-. +#endif .Lcatch_types_end: #ifndef _WIN32 .section .note.GNU-stack,"",@progbits From a0bd257a0945110e7d44ddfcf416908a9d57b899 Mon Sep 17 00:00:00 2001 From: alosta Date: Tue, 13 Oct 2026 20:42:44 +0100 Subject: [PATCH 218/353] feat(libc): implement swprintf_s Add the missing wide secure formatting exports required by PPSA15552 (Dead Cells), whose load failed with `undefined symbol: Im55VJ-Bekc`: swprintf_s and its va_list counterpart follow C11 Annex K.3.9.1.4 (the complete output, including the terminator, must fit), while vsnwprintf_s is the va_list counterpart of snwprintf_s (K.3.9.1.3, truncation with full length). The export names produce NIDs Im55VJ-Bekc, oDoV9tyHTbA and 8SKVXgeK1wY respectively. Document the new functions in docs/dev/debt/libc.md. --- core/libs/prx/libc/src/FormattingWide.cpp | 57 +++++++++++++++++++++++ core/libs/tests/GuestWideFormatting.cpp | 36 ++++++++++++++ docs/dev/debt/libc.md | 2 + 3 files changed, 95 insertions(+) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 1aa78ba067..e9a5661781 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -259,6 +259,63 @@ int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const return result; } +int APS5_VABI vswprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI swprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && size != 0 && size <= RsizeMax) { + APS5_VA_BEGIN(format); + try { + const std::u16string text = FormatWide(format, reinterpret_cast(args), true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + APS5_VA_END(); + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() <= static_cast(INT_MAX)) { + const std::size_t copied = text.size() < size - 1 ? text.size() : size - 1; + std::memcpy(buffer, text.data(), copied * sizeof(char16_t)); + buffer[copied] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { APS5_VA_BEGIN(format); int result = -1; diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index adba600ad6..f372fd47e1 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -10,6 +10,9 @@ extern "C" int APS5_VABI vswprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI snwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vswprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); +extern "C" int APS5_VABI swprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vsnwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI swprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); extern "C" std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); extern "C" std::size_t APS5_VABI wcsftime_nid_postfix(char16_t*, std::size_t, const char16_t*, const GuestTm*); @@ -148,6 +151,38 @@ static void CheckGroupingFlag() { "snwprintf_s ' flag"); } +static void CheckSecureWideBoundary() { + constexpr std::size_t wideRsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + char16_t buffer[4]; + for (auto& unit : buffer) unit = u'x'; + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n") < 0 && buffer[0] == 0, "swprintf_s accepted-boundary failure clears"); + Require(buffer[1] == u'y', "swprintf_s does not clear beyond the first unit"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n") < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "swprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vswprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vswprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vsnwprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vsnwprintf_s oversized size leaves the buffer untouched"); +} + static void CheckSwprintf() { char16_t buffer[8]; for (auto& unit : buffer) unit = u'x'; @@ -244,6 +279,7 @@ int main() { CheckPointer(); CheckCount(); CheckSwprintf(); + CheckSecureWideBoundary(); CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0a6b4a8324..8fa6392110 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -20,6 +20,8 @@ - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s and memmove_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified From 3ebad9045e827568efa5a1987cbe9a6aded4abd4 Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:00:11 +0200 Subject: [PATCH 219/353] perf(agc): expand draw recipes to support depth targets --- .../Execution/include/Recipe.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 13 +- .../tests/execution/DepthBiasPerFace.cpp | 147 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 51c74e1a0a..9a4dc16db6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp @@ -84,6 +84,7 @@ struct DrawRecipe { std::optional masked; std::set fragmentOutputs; VkPipelineStageFlags shaderStages = 0; + bool hasDepth = false; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 68f487b489..9ccc4e038a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2144,7 +2144,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2163,6 +2163,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->masked = masked; recipe->fragmentOutputs = inputs.fragmentOutputs; recipe->shaderStages = inputs.shaderStages; + recipe->hasDepth = state.depth.has_value(); *recipeOut = std::move(recipe); } timing.Mark("draw_and_resource_release"); @@ -2440,7 +2441,9 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); - Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state"); + if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone); + const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u); + Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state"); auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe); if (inputs.nothing) { result.recorded = true; @@ -2466,6 +2469,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone); targets.push_back(std::move(resident)); } + if (recipe.hasDepth) { + timer.phase(PhaseSetup); + const auto depthView = DepthSurfaceView(context, *state.depth); + timer.phase(PhaseReadTarget); + if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); + } timer.phase(PhasePrepare); // The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed // key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp index d54ab03997..f672de40ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp @@ -1,9 +1,19 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.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 @@ -124,6 +134,142 @@ void Check(bool frontDrawn, bool backDrawn, std::string_view what) { } } +class GuestBlock { +public: + static constexpr std::size_t BlockBytes = 65536; + + 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, "cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +void DepthDrawRecipe(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t TargetWidth = 128; + constexpr std::uint32_t TargetHeight = 128; + const AgcDriver::Graphics::ColorTargetLayout colorLayout(TargetWidth, TargetHeight, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + + const GuestBlock guestVertices; + const GuestBlock guestTarget; + alignas(256) static std::array RecipeDepthMemory{}; + alignas(256) static std::array RecipeAlternateDepthMemory{}; + + std::memset(guestTarget.Data(), static_cast(Kept), GuestBlock::BlockBytes); + RecipeDepthMemory.fill(1.0f); + RecipeAlternateDepthMemory.fill(1.0f); + + auto* targetVertices = reinterpret_cast*>(guestVertices.Data()); + for (std::size_t index = 0; index < Corners.size(); ++index) { + targetVertices[index] = {Corners[index][0] * 2.0f / TargetWidth - 1.0f, 1.0f - Corners[index][1] * 2.0f / TargetHeight, SurfaceDepth, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(targetVertices, 16u, static_cast(Corners.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {guestTarget.Address(), {TargetWidth, TargetHeight}, VK_FORMAT_R8G8B8A8_UNORM, colorLayout.Bytes(), 0xe4u, AgcDriver::Graphics::ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_LESS; + state.depthBias = true; + state.depthBiasConstant = -Bias; + state.depthBiasPerFace = false; + state.backDepthBiasConstant = -Bias; + state.renderExtent = {TargetWidth, TargetHeight}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(TargetHeight), static_cast(TargetWidth), -static_cast(TargetHeight), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {TargetWidth, TargetHeight}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Corners.size()), 0, 1, 0, false}; + + std::shared_ptr recipe; + device.Draw(state, draw, shaders, {}, &recipe); + device.WaitIdle(); + Require(recipe != nullptr, "a direct draw with depth target must produce a recipe"); + Require(recipe->hasDepth, "the recipe must record depth target presence"); + Require(recipe->targetViews.size() == 2, "the recipe must record color and depth views"); + + const auto outcome = device.DrawFromRecipe(state, draw, shaders, {}, recipe); + device.WaitIdle(); + Require(outcome == AgcDriver::RecipeOutcome::Recorded, "drawing with depth from recipe must record successfully"); + + auto mutatedAddressState = state; + mutatedAddressState.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeAlternateDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + const auto rejectedAddressOutcome = device.DrawFromRecipe(mutatedAddressState, draw, shaders, {}, recipe); + Require(rejectedAddressOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target address changes"); + + auto removedDepthState = state; + removedDepthState.depth.reset(); + const auto rejectedDepthOutcome = device.DrawFromRecipe(removedDepthState, draw, shaders, {}, recipe); + Require(rejectedDepthOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target is removed"); +} + } int main() { @@ -138,6 +284,7 @@ int main() { Check(true, true, "both faces pulled closer"); Draw(*device, -Bias, 0.0f); Check(true, false, "only front faces biased"); + DepthDrawRecipe(*device); std::puts("per-face depth bias tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a5d4bcb316..b49941b352 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -34,6 +34,7 @@ - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [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. - [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. From d6f67d1be2ef7dc7cc0e9167cb7fc79197e381cc Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:14:39 +0200 Subject: [PATCH 220/353] fix(agc): keep fused geometry programs out of the caller's memory sceAgcFuseShaderHalves copied both halves' code, the fused header and its tables into the caller's memory and registered the program at that address. PPSA02100 fuses different halves into the same memory a few calls apart, while draws that use the earlier result are still queued. The registry keeps one program per code address, so those draws were compiled from the later program: its code read a user-data word the bound layout does not have, and the device reported "BDA access failed" for the mesh stage. The PS5 5.02 module writes only the back half's register list into that memory (sceAgcGetFusedShaderSize reports eight bytes per register, alignment 4) and leaves the code where the halves are. The fused code, header and tables now live in memory libSceAgc owns, one program per distinct pair of halves, registered once. The caller's memory receives the fused register list and the size query reports that list. A second fusion of the same halves returns the same program; halves whose contents changed get a new one. The driver looked a header up by comparing its fields with the registered header, including the register list's address. A header whose register list is a copy elsewhere with the same contents now matches too, which is what the title passes after a fusion. --- .../prx/libSceAgc/Misc/src/ShaderFusion.cpp | 74 +++++++++++++++---- .../libs/prx/libSceAgc/tests/ShaderFusion.cpp | 55 ++++++++++---- .../src/Driver/Shaders/ShaderRegistry.cpp | 13 +++- .../tests/execution/PreparedShaders.cpp | 6 ++ 4 files changed, 118 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 499217f13b..8bae004ff1 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,8 +31,7 @@ constexpr std::uint32_t SCodeEnd = 0xbf9f0000u; constexpr std::uint32_t SNop = 0xbf800000u; constexpr std::uint32_t SSetpcS6 = 0xbe802006u; constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u; -constexpr std::uint8_t FusedCodeAlignmentLog2 = 8; -constexpr std::size_t FusedCodeAlignment = std::size_t{1} << FusedCodeAlignmentLog2; +constexpr std::size_t FusedCodeAlignment = 256; constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'}; constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t); constexpr std::uint32_t Rsrc1VgprMask = 0x3fu; @@ -191,14 +192,45 @@ void SetProgramAddress(std::vector& regs, std::uint32_t loOffset throw std::runtime_error("sceAgcFuseShaderHalves_0200: back half has no program address register pair"); } -int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { - constexpr auto fn = "sceAgcFuseShaderHalves_0200"; - if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); - auto layout = ComputeGeometryLayout(fn, front, back); - const auto scratch = reinterpret_cast(scratch_mem); - const auto base = AlignUp(scratch, FusedCodeAlignment); +struct FusedProgram { + std::vector storage; + Shader* header; +}; + +std::mutex FusedProgramsMutex; +std::map, FusedProgram> FusedPrograms; + +void AppendBytes(std::vector& bytes, const void* data, std::size_t size) { + if (size == 0) return; + const auto* begin = static_cast(data); + bytes.insert(bytes.end(), begin, begin + size); +} + +std::vector FusionKey(const Shader* front, const Shader* back, const GeometryLayout& layout) { + std::vector key; + constexpr auto headerBytes = offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers); + AppendBytes(key, front, headerBytes); + AppendBytes(key, back, headerBytes); + AppendBytes(key, const_cast(front->code), layout.frontBytes); + AppendBytes(key, const_cast(back->code), back->shader_size); + AppendBytes(key, layout.sh.data(), layout.sh.size() * sizeof(ShaderRegister)); + AppendBytes(key, layout.cx.data(), layout.cx.size() * sizeof(ShaderRegister)); + AppendBytes(key, back->specials, back->special_sizes_bytes); + AppendBytes(key, front->input_semantics, front->num_input_semantics * sizeof(ShaderSemantic)); + AppendBytes(key, back->output_semantics, back->num_output_semantics * sizeof(ShaderSemantic)); + if (back->user_data == nullptr) return key; + AppendBytes(key, back->user_data, offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData::sharp_resource_count)); + AppendBytes(key, back->user_data->direct_resource_offset, back->user_data->direct_resource_count * sizeof(std::uint16_t)); + for (std::size_t i = 0; i < layout.sharpOffsets.size(); ++i) AppendBytes(key, back->user_data->sharp_resource_offset[i], back->user_data->sharp_resource_count[i] * sizeof(ShaderSharp)); + return key; +} + +FusedProgram BuildFusedProgram(const char* fn, const Shader* front, const Shader* back, GeometryLayout& layout) { + FusedProgram program{std::vector(layout.totalBytes + FusedCodeAlignment - 1), nullptr}; + const auto storage = reinterpret_cast(program.storage.data()); + const auto base = AlignUp(storage, FusedCodeAlignment); if ((base & SHADER_BASE_ALIGN_MASK) != 0) Fail(fn, "fused shader memory is not a valid program address"); - auto* memory = static_cast(scratch_mem) + (base - scratch); + auto* memory = program.storage.data() + (base - storage); std::memcpy(memory, const_cast(front->code), layout.frontBytes); for (std::size_t offset = layout.frontBytes; offset < layout.backOffset; offset += sizeof(SNop)) std::memcpy(memory + offset, &SNop, sizeof(SNop)); std::memcpy(memory + layout.backOffset, const_cast(back->code), back->shader_size); @@ -241,10 +273,24 @@ int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* fused.type = static_cast(ShaderBinaryType::Gs); fused.num_sh_registers = static_cast(layout.sh.size()); fused.num_cx_registers = static_cast(layout.cx.size()); - auto* header = reinterpret_cast(memory + layout.headerOffset); - *header = fused; + program.header = reinterpret_cast(memory + layout.headerOffset); + *program.header = fused; + AgcDriverRegisterShader_nid_postfix(program.header); + return program; +} + +int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { + constexpr auto fn = "sceAgcFuseShaderHalves_0200"; + if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); + auto layout = ComputeGeometryLayout(fn, front, back); + auto key = FusionKey(front, back, layout); + std::lock_guard lock(FusedProgramsMutex); + auto found = FusedPrograms.find(key); + if (found == FusedPrograms.end()) found = FusedPrograms.emplace(std::move(key), BuildFusedProgram(fn, front, back, layout)).first; + Shader fused = *found->second.header; + std::memcpy(scratch_mem, fused.sh_registers, std::size_t{fused.num_sh_registers} * sizeof(ShaderRegister)); + fused.sh_registers = static_cast(scratch_mem); *fused_result = fused; - AgcDriverRegisterShader_nid_postfix(header); return 0; } @@ -306,8 +352,8 @@ int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; if (GeometryHalves(front)) { const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back); - dst->m_size = layout.totalBytes + FusedCodeAlignment - 1; - dst->m_align = FusedCodeAlignmentLog2; + dst->m_size = layout.sh.size() * sizeof(ShaderRegister); + dst->m_align = 4; return 0; } dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister); diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index c0a6a4eda6..2c901efc5b 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -129,21 +129,20 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { Halves halves(terminator); SizeAlign size{}; check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed"); - check(size.m_align == 8, "fused shaders are not 256-byte aligned"); - check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header"); + check(size.m_size == 7 * sizeof(ShaderRegister) && size.m_align == 4, "fused size is not the fused register list"); std::vector storage(size.m_size + 512, 0xcd); auto* scratch = aligned(storage, 256) + misalignment; Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed"); for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size"); - auto* memory = scratch + (256 - misalignment) % 256; - const auto base = reinterpret_cast(memory); - const auto scratchBase = reinterpret_cast(scratch); + const auto base = reinterpret_cast(fused.code); + const auto* memory = reinterpret_cast(base); check(fused.type == static_cast(ShaderRegs::ShaderBinaryType::Gs), "fused shader is not a GS"); - check(fused.code == memory && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); + check(base % 256 == 0 && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); check(word(memory, 0) == SMovS0 && word(memory, 1) == VMovV0, "front program was not copied"); for (std::size_t i = 2; i < 64; ++i) check(word(memory, i) == SNop, "front program does not fall through to the back half"); check(std::memcmp(memory + 256, halves.backCode.data(), halves.back.shader_size) == 0, "back program was not copied"); + check(reinterpret_cast(fused.sh_registers) == reinterpret_cast(scratch), "fused registers are not in the caller's memory"); const auto* backLo = find(fused, 0x088u); const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast((base + 256) >> 8u), "back program address was not patched"); @@ -158,17 +157,42 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { check(fused.num_output_semantics == 1 && fused.output_semantics[0].semantic == 3, "back outputs were not kept"); check(fused.scratch_size_dw_per_thread == 16, "scratch size is not the larger half's"); check(fused.specials != nullptr && fused.specials != halves.back.specials && fused.specials->ge_cntl.value == 0x77u, "back specials were not copied"); - const auto inside = [&](const void* pointer) { - const auto address = reinterpret_cast(pointer); - return address >= scratchBase && address < scratchBase + size.m_size; + const auto inside = [&](std::uintptr_t address) { + const auto begin = reinterpret_cast(storage.data()); + return address >= begin && address < begin + storage.size(); }; - check(inside(fused.sh_registers) && inside(fused.cx_registers) && inside(fused.specials) && inside(fused.input_semantics) && inside(fused.output_semantics), "fused tables are not in the fused memory"); - check(inside(fused.user_data) && inside(fused.user_data->direct_resource_offset) && inside(fused.user_data->sharp_resource_offset[1]), "fused user data is not in the fused memory"); + const auto program = [&](const void* pointer) { + const auto address = reinterpret_cast(pointer); + return !inside(address) && address >= base + fused.shader_size && address < base + fused.shader_size + fused.header_size + alignof(Shader); + }; + check(!inside(base) && program(fused.cx_registers) && program(fused.specials) && program(fused.input_semantics) && program(fused.output_semantics), "fused tables are not beside the fused code"); + check(program(fused.user_data) && program(fused.user_data->direct_resource_offset) && program(fused.user_data->sharp_resource_offset[1]), "fused user data is not beside the fused code"); check(fused.user_data->direct_resource_count == 2 && fused.user_data->direct_resource_offset[1] == 9u && fused.user_data->sharp_resource_offset[1][0].offset_dw == 12 && fused.user_data->sharp_resource_offset[0] == nullptr && fused.user_data->eud_size_dw == 4, "fused user data is wrong"); - const auto headerOffset = static_cast(size.m_size) - 255 - fused.header_size; - check(headerOffset % alignof(Shader) == 0 && headerOffset >= fused.shader_size, "fused header is misplaced"); - const auto* header = reinterpret_cast(memory + headerOffset); - check(header->code == fused.code && header->sh_registers == fused.sh_registers && header->user_data == fused.user_data && header->header_size == fused.header_size && header->shader_size == fused.shader_size && header->type == fused.type && header->num_sh_registers == fused.num_sh_registers, "fused memory has no self-contained header"); +} + +void testScratchReuse() { + Halves first; + Halves second; + second.backCode[0] = VMovV0; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &first.front, &first.back) == 0, "fused size query failed"); + std::vector scratch(size.m_size); + Shader firstFused{}; + Shader secondFused{}; + check(sceAgcFuseShaderHalves_0200(&firstFused, &first.front, &first.back, scratch.data()) == 0, "first fusion failed"); + const auto firstCode = reinterpret_cast(firstFused.code); + check(sceAgcFuseShaderHalves_0200(&secondFused, &second.front, &second.back, scratch.data()) == 0, "second fusion failed"); + const auto secondCode = reinterpret_cast(secondFused.code); + check(firstCode != secondCode, "two fused programs share one code address"); + check(word(reinterpret_cast(firstCode), 64) == SMovS0, "fusing into reused memory overwrote the first fused program"); + check(word(reinterpret_cast(secondCode), 64) == VMovV0, "second fused program has the wrong back half"); + Shader again{}; + check(sceAgcFuseShaderHalves_0200(&again, &first.front, &first.back, scratch.data()) == 0, "repeated fusion failed"); + check(reinterpret_cast(again.code) == firstCode, "the same halves were fused into a second program"); + first.backCode[0] = VMovV0; + Shader changed{}; + check(sceAgcFuseShaderHalves_0200(&changed, &first.front, &first.back, scratch.data()) == 0, "fusion of changed halves failed"); + check(reinterpret_cast(changed.code) != firstCode && word(reinterpret_cast(firstCode), 64) == SMovS0, "changed halves reused the earlier fused program"); } void testRejections() { @@ -252,6 +276,7 @@ int main() { testFusion(0xa0); testFusion(4); testFusion(0, SSwappcNullS6); + testScratchReuse(); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); 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 4ad3671f97..a146960d21 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -324,6 +324,17 @@ static Shader GuestShaderFields(const Shader* shader) { return fields; } +static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + const auto* bound = used.sh_registers; + used.sh_registers = registered.sh_registers; + if (std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + const auto address = reinterpret_cast(registered.sh_registers); + const std::size_t bytes = registered.num_sh_registers * sizeof(ShaderRegister); + if (bytes == 0 || bound == nullptr || address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size() || snapshot.header.size() - (address - snapshot.headerAddress) < bytes) return false; + GuestMemory::CheckRange(bound, bytes, alignof(ShaderRegister)); + return std::memcmp(static_cast(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0; +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -333,7 +344,7 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { std::memcpy(&used, static_cast(shader), sizeof(Shader)); std::memcpy(®istered, snapshot.header.data(), sizeof(Shader)); used.user_data = registered.user_data; - return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || FusedHullHeader(snapshot, used, registered); + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); } std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index d1b66658cc..c4fb6eba1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -389,6 +389,12 @@ void SharedCodeHeaders() { Shader copy = vertex.shader; copy.user_data = nullptr; AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + auto moved = vertex.registers; + copy.sh_registers = moved.data(); + AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + moved[2].value ^= 2u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); + copy.sh_registers = vertex.shader.sh_registers; copy.target ^= 1u; ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); std::vector commands; From e94f12bd17ee7e9fe8de69a1ae7c019c56a4a89f Mon Sep 17 00:00:00 2001 From: Tenith01 Date: Wed, 7 Oct 2026 10:07:21 +0530 Subject: [PATCH 221/353] fix(videoout): disable DirectInput joystick enumeration on Windows --- core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 5b3dcdc23b..3da668b1c2 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -259,6 +259,9 @@ VideoOutDriver& VideoOutDriver::Get() { VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); +#ifdef _WIN32 + SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); +#endif if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } From 56d1eda2f05d9f67f1bfe9d5a62399022af85f07 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 13:12:24 +1300 Subject: [PATCH 222/353] docs(videoout): record disabled DirectInput controller backend Co-authored-by: Tenith01 <102300949+Tenith01@users.noreply.github.com> --- 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 a20f0876af..1d94263abb 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -23,3 +23,4 @@ - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. +- [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device From 955b56a2b5841829444bca2c56f0b619613db6e0 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:50:40 -0400 Subject: [PATCH 223/353] perf(agc): trust write stamps for snapshots of partially committed textures A sampled texture over a partially resident surface (a texture array whose heap the guest commits on demand) never got a write generation: CollectWrites fails on a range with reserved pages, so every cache hit compared the whole committed part with its snapshot. CollectCommittedWrites collects the committed runs one by one and returns the runs with the generation; the cache entry records them, and a hit skips the compare only when the committed runs are the same and UnchangedCommittedSince holds over them. A commit, decommit, view mapping or unmapping inside the surface changes the runs or stamps the new pages, so it is compared as before. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/GuestMemory.hpp | 2 + .../Execution/src/GuestMemory.cpp | 24 ++ .../Graphics/src/ShaderResources.cpp | 24 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 137 ++++++++ .../execution/PartiallyResidentSnapshot.cpp | 293 ++++++++++++++++++ 5 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 3932b5dc58..6e174a34ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -72,6 +72,8 @@ struct UnchangedQuery { }; bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); bool UnchangedSinceAll(std::span queries); +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges); +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation); // Returns the generation the blocks were stamped with (0 when the arena is not write-watched): // UnchangedSince(range, it) holds until the next store over the range. std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b284..9456db6c61 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1263,6 +1263,30 @@ bool UnchangedSinceAll(std::span queries) { return true; } +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges) { + ranges.clear(); + if (bytes == 0 || bytes > std::numeric_limits::max() - address || !WriteWatched()) return 0; + auto committed = CommittedRanges(address, bytes); + auto generation = TrackerGeneration(); + for (const auto& [begin, end] : committed) { + generation = CollectWrites(begin, static_cast(end - begin)); + if (generation == 0) return 0; + } + ranges = std::move(committed); + return generation; +} + +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || generation == 0) return false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || !tracker.covers(begin, static_cast(end - begin)) || tracker.newestStamp(begin, end) > generation) return false; + } + return true; +} + std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function()>& store) { if (bytes == 0) return 0; const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 552145a607..a0bfa681be 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -111,6 +111,7 @@ struct CachedTexture { std::uint64_t sourceVersion = 0; std::uint64_t accounted = 0; std::uint64_t present = 0; + std::optional>> committed; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -631,6 +632,15 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, snapshot, depthCompare); } + auto snapshotGeneration = generation; + std::optional>> committed; + if (source == nullptr && generation == 0) { + std::vector> ranges; + if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) { + snapshotGeneration = partial; + committed = std::move(ranges); + } + } auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); @@ -659,8 +669,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes); }; - if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { - it->generation = generation; + const auto unchanged = [&] { + if (it->committed != committed) return false; + return committed.has_value() ? GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + }; + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { + it->generation = snapshotGeneration; + it->committed = std::move(committed); touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -671,7 +686,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, generation}; + CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -3268,7 +3284,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) auto& cache = Textures(); std::lock_guard lock(cache.mutex); const auto it = findSampledEntry(cache, context.device, record.words, record.components); - if (it == cache.entries.end() || it->texture != record.texture) return nullptr; + if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr; if (it->source == nullptr) it->generation = record.generation; touchTexture(cache, it); logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()}); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index f839edd79d..d3cf8a962a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -562,6 +562,142 @@ void CheckUnwatchedImport() { #endif } +class PartlyCommitted { +public: + static constexpr std::size_t Bytes = 4 * Block; + + PartlyCommitted() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block)); +#else + void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the partly committed range"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + Bytes != begin + Bytes + Block) munmap(reinterpret_cast(aligned + Bytes), begin + Block - aligned); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve the partly committed range"); + } + + ~PartlyCommitted() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes); + munmap(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); +#endif + } + + PartlyCommitted(const PartlyCommitted&) = delete; + PartlyCommitted& operator=(const PartlyCommitted&) = delete; + + void Commit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block); +#else + Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + void Decommit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block); + Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast(memory) + block * Block; } + +private: + std::uint8_t* memory = nullptr; +}; + +using Runs = std::vector>; + +std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) { + BumpCollectEpoch(); + return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs); +} + +void CheckCommittedRanges() { + PartlyCommitted range; + const auto base = range.Address(); + range.Commit(0); + std::memset(range.Data(), 0x11, Block); + Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole"); + Runs runs; + const Runs first{{base, base + Block}}; + const auto synced = CollectCommitted(range, runs); + Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected"); + Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed"); + + range.Data()[8] = 0x22; + const auto written = CollectCommitted(range, runs); + Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed"); + Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed"); + Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed"); + + const auto beforeCommit = CollectCommitted(range, runs); + range.Commit(2); + const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}}; + const auto grown = CollectCommitted(range, runs); + Require(grown != 0 && runs == both, "a block committed inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit"); + Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect"); + + range.Decommit(2); + range.Commit(2); + const auto recommitted = CollectCommitted(range, runs); + Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged"); + + range.Decommit(2); + Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed"); + +#ifdef _WIN32 + HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + struct Sections { + HANDLE first; + HANDLE second; + ~Sections() { + if (first != nullptr) CloseHandle(first); + if (second != nullptr) CloseHandle(second); + } + } sections{firstSection, secondSection}; + Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range"); + const auto beforeMap = CollectCommitted(range, runs); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}}; + const auto mapped = CollectCommitted(range, runs); + Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping"); + Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed"); +#endif + + range.Decommit(0); + Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty"); + Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts"); +} + int main() { try { if (!WriteWatched()) { @@ -587,6 +723,7 @@ int main() { CheckCollectBoundaries(); CheckImportWindow(); CheckUnwatchedImport(); + CheckCommittedRanges(); } catch (const std::exception& error) { std::cerr << "write tracking test failed: " << error.what() << "\n"; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp new file mode 100644 index 0000000000..f3270ac389 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -0,0 +1,293 @@ +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.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 +#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 namespace AgcDriver::Graphics; + +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 BlockBytes = 65536; +constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr int Skipped = 77; + +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 extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + 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_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderInt64 = VK_TRUE; + address.pNext = &bytes; + const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + 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; + context.bufferDeviceAddress = true; + 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.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearCachedTextures(context.device); + 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{}; +}; + +class ReservedSurface { +public: + ReservedSurface() { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~ReservedSurface() { + GuestAllocations::Mutation().Remove(block); + Decommit(0, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + } + + ReservedSurface(const ReservedSurface&) = delete; + ReservedSurface& operator=(const ReservedSurface&) = delete; + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(block + offset, bytes, PAGE_READWRITE, bytes); +#else + Require(mmap(block + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot commit a page"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + for (std::size_t page = 0; page < bytes; page += 4096) static_cast(static_cast(block)[offset + page]); + } + + void Decommit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block + offset, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block + offset, bytes); + Require(mmap(block + offset, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot decommit a page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + } + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +std::atomic surfaceBegin{0}; +std::atomic surfaceEnd{0}; +std::atomic surfaceReads{0}; + +void CountReads(std::uint64_t address, std::size_t bytes) { + if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); +} + +std::array Descriptor(std::uint64_t address) { + 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 Run(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == SurfaceBytes, "the test descriptor does not describe the reserved surface"); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the partially resident surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the partially resident surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the partially resident surface was compared with guest memory " + when + " although nothing changed (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the partially resident surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + sample("again", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write to its committed part", true, false); + sample("after the re-read of the written part", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store into its committed part", true, true); + sample("after the compare that followed the driver store", false, true); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was committed", true, true); + sample("after the compare that followed the commit", false, true); + std::memset(surface.Data() + 2 * BlockBytes, 0x33, BlockBytes); + sample("after a CPU write to the newly committed page", true, false); + sample("after the re-read of the newly committed page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted and committed again", true, false); + sample("after the re-read of the recommitted page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted", true, true); + sample("after the compare that followed the decommit", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +} + +int main() { + try { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory has no write watch"); + return Skipped; + } + std::optional device; + try { + device.emplace(); + } 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 Skipped; + } + { + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + ReservedSurface surface; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Run(context, surface); + ClearCachedTextures(context.device); + } + device.reset(); + std::puts("partially resident snapshot tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 8d8fe39eede50d0bce27a1d8488ca80e1bec7863 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:01:20 -0400 Subject: [PATCH 224/353] fix(agc): flush pending stores before a partial snapshot hit A partially resident snapshot cannot be reused from committed write stamps while a recorder batch still owns an overlapping completion store. Make that case compare through the guest-memory flush hook before deciding whether the snapshot is unchanged. Cover copied completion stores in both open and submitted batches, then verify that unchanged snapshots are reused and an unrelated pending write does not synchronize. The focused regression fails on the published PR head and passes with the guard. The existing fully committed snapshot path is unchanged. Co-Authored-By: Codex --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../Graphics/src/ShaderResources.cpp | 2 +- .../execution/PartiallyResidentSnapshot.cpp | 50 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4fc24a5f2b..3fdab7d9a8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1235,6 +1235,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) + set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index a0bfa681be..a1f9a66617 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -671,7 +671,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spancommitted != committed) return false; - return committed.has_value() ? GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); }; if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { it->generation = snapshotGeneration; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index f3270ac389..4bdf081efe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -19,6 +20,7 @@ #endif #include #include +#include #include #include #include @@ -29,6 +31,8 @@ #include #include #include +#include +#include #include namespace { @@ -258,9 +262,50 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void PendingStores(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + Recorder recorder(context); + recorder.Activate(); + const auto lookup = [&] { + AgcDriver::GuestMemory::BumpCollectEpoch(); + return CachedSampledTexture(context, words); + }; + auto previous = lookup(); + Require(previous != nullptr && !previous->ViewsStorageImage(), "the pending-store surface did not produce a snapshot"); + Require(lookup() == previous, "the unchanged pending-store surface did not reuse its snapshot"); + for (const bool submitted : {false, true}) { + const auto landed = std::make_shared(false); + const auto value = submitted ? std::byte{0x99} : std::byte{0x77}; + std::vector original(reinterpret_cast(surface.Data()), reinterpret_cast(surface.Data()) + 0x1000); + recorder.NotePendingWrite(address, 0x1000); + recorder.OnComplete([address, landed, value, original = std::move(original)] { + const std::vector current(0x1000, value); + AgcDriver::GuestMemory::WriteChanged(address, current, original); + *landed = true; + }); + if (submitted) recorder.Submit(); + const auto next = lookup(); + Require(*landed, "the partially resident lookup returned before its pending copied store completed"); + Require(next != previous, "the partially resident lookup reused the snapshot from before its pending copied store"); + Require(surface.Data()[0] == std::to_integer(value), "the pending copied store did not update guest memory"); + Require(lookup() == next, "the partially resident lookup did not reuse the snapshot after its pending copied store"); + previous = next; + } + const auto unrelated = std::make_shared(false); + recorder.NotePendingWrite(address + SurfaceBytes, BlockBytes); + recorder.OnComplete([unrelated] { *unrelated = true; }); + Require(lookup() == previous, "an unrelated pending write replaced the partially resident snapshot"); + Require(!*unrelated, "a partially resident lookup waited for an unrelated pending write"); + recorder.Sync(); + Require(*unrelated, "the unrelated pending completion did not run when synchronized"); } -int main() { +} + +int main(int argc, char** argv) { try { if (!AgcDriver::GuestMemory::WriteWatched()) { std::puts("skipped, guest memory has no write watch"); @@ -280,7 +325,8 @@ int main() { context.detiler = &detiler; ReservedSurface surface; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - Run(context, surface); + if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + else Run(context, surface); ClearCachedTextures(context.device); } device.reset(); From d1527840ea398973f041da125e7504220d390d25 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 20:30:24 -0400 Subject: [PATCH 225/353] fix(libkernel): retire stack metadata before releasing mappings --- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 4 +- core/libs/tests/GuestPthreadStack.cpp | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index cdbf5c9ab7..caeb5c291d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -125,8 +125,8 @@ static void AllocateGuestStack(PthreadPrivate* thread, std::size_t usableBytes) static void FreeGuestStack(PthreadPrivate* thread) { if (thread->guestStack == nullptr) return; void* const mapping = thread->guestStack; - DoMunmap(thread->guestStack, thread->guestStackBytes); UnregisterGuestStackMapping(mapping); + DoMunmap(thread->guestStack, thread->guestStackBytes); thread->guestStack = nullptr; thread->guestStackBytes = 0; } @@ -152,8 +152,8 @@ static void ReapRetiredStacks() { } } for (const auto& stack : ready) { - DoMunmap(stack.address, stack.bytes); UnregisterGuestStackMapping(stack.address); + DoMunmap(stack.address, stack.bytes); } } diff --git a/core/libs/tests/GuestPthreadStack.cpp b/core/libs/tests/GuestPthreadStack.cpp index 21045925e5..5fcbe7ac52 100644 --- a/core/libs/tests/GuestPthreadStack.cpp +++ b/core/libs/tests/GuestPthreadStack.cpp @@ -1,4 +1,14 @@ #include "SceTypes.hpp" +#ifdef __linux__ +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#endif #include #include #include @@ -12,6 +22,9 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackAddress, std::size_t* stackSize); Pthread APS5_VABI scePthreadSelf(); +#ifdef __linux__ +int APS5_VABI scePthreadDetach(Pthread thread); +#endif } static constexpr int SCE_OK = 0; @@ -64,7 +77,41 @@ static void* APS5_VABI Worker(void* arg) { return arg; } +#ifdef __linux__ +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end); +static std::atomic observedUnmaps{0}; +static std::atomic retiredStackWord{0}; +static std::atomic retiredUnmapped{false}; +static std::atomic detachedDone{false}; +static int detachedTid = 0; + +static void* APS5_VABI DetachedWorker(void*) { + int local = 0; + detachedTid = static_cast(syscall(SYS_gettid)); + retiredStackWord.store(reinterpret_cast(&local), std::memory_order_relaxed); + detachedDone.store(true, std::memory_order_release); + return nullptr; +} + +static void* APS5_VABI ReaperWorker(void*) { + return nullptr; +} + +static void ObserveUnmap(std::uintptr_t address, std::size_t bytes) { + if (GuestThreadStackHostRangeOverlaps(address, address + bytes)) { + std::fprintf(stderr, "Retired stack still owns host-only pages after unmap\n"); + std::abort(); + } + observedUnmaps.fetch_add(1, std::memory_order_relaxed); + const auto retired = retiredStackWord.load(std::memory_order_relaxed); + if (retired >= address && retired - address < bytes) retiredUnmapped.store(true, std::memory_order_relaxed); +} +#endif + int main() { +#ifdef __linux__ + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&ObserveUnmap); +#endif for (auto stackSize : STACK_SIZES) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); @@ -76,4 +123,25 @@ int main() { Require(scePthreadJoin(thread, &result) == SCE_OK); Require(result == &stackSize); } +#ifdef __linux__ + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, nullptr, DetachedWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadDetach(detached) == SCE_OK); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!detachedDone.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + const auto task = "/proc/self/task/" + std::to_string(detachedTid); + while (access(task.c_str(), F_OK) == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Pthread reaper = nullptr; + Require(scePthreadCreate(&reaper, nullptr, ReaperWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(reaper, nullptr) == SCE_OK); + Require(retiredUnmapped.load(std::memory_order_relaxed)); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); + Require(observedUnmaps.load(std::memory_order_relaxed) != 0); +#endif } From ca95f740977bfcd3da88ec61794993b7cf58169d Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:37:39 +0200 Subject: [PATCH 226/353] perf(agc-driver): join the imports of adjacent ranges of any size that a buffer is bound exactly over A bound buffer is joined across adjacent registered ranges only when every one of them is larger than the 8 MiB window. PPSA25344's loading binds 16 MiB and 4 MiB buffers whose backing was registered in two to six adjacent pieces with the same access, some of them smaller than the window, so each use of such a buffer was copied on the CPU: up to 348 GB in the first 300 s of loading. A bound buffer that starts at the first crossed range's start and ends at the last one's end now joins those ranges whatever their size. A heap grown in pieces matches that only for a binding over exactly its current pieces, so growth beside a joined import does not re-import it. joinedCovers, used when a joined import retires, accepts any adjacent run of ranges. importJoinTests binds exactly over two 64 KiB pieces of a partly filled window and requires one import of both; a binding across them that starts inside the first piece stays apart. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestBufferMemory.cpp | 21 ++++++++++++------- .../libSceAgcDriver/tests/BdaResources.cpp | 10 ++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 2a159924fb..3a58f722d0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -633,14 +633,11 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } -ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { - const auto window = importWindow.load(std::memory_order_relaxed); +ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { auto last = leasedIndexContaining(lease, begin); - if (window == 0 || last == lease.size()) return {}; + if (last == lease.size()) return {}; const auto first = last; - for (;;) { - if (lease[last]->bytes <= window) return {}; - if (lease[last]->address + lease[last]->bytes >= end) break; + while (lease[last]->address + lease[last]->bytes < end) { if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; ++last; } @@ -650,8 +647,18 @@ ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, s return run; } +ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto window = importWindow.load(std::memory_order_relaxed); + if (window == 0) return {}; + auto run = crossedRun(lease, begin, end); + if (run.ranges.empty()) return run; + if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run; + if (begin == run.base && end == run.base + run.bytes) return run; + return {}; +} + bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { - return !joinedRun(lease, begin, end).ranges.empty(); + return !crossedRun(lease, begin, end).ranges.empty(); } template diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ac1c3b11a4..213590932a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -262,12 +262,12 @@ void importJoinTests(const Context& context) { else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; - const auto bind = [&](std::uintptr_t address) { + const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) { GuestBufferMemory plain(importing); - plain.AddReadable(address, 32); + plain.AddReadable(address, bytes); plain.Upload(false); std::uint32_t adjustment = 0; - const auto view = plain.Descriptor(address, 32, adjustment); + const auto view = plain.Descriptor(address, bytes, adjustment); const auto copied = plain.CopiedBytes(); plain.WriteBack(); return std::pair{view.buffer, copied}; @@ -298,6 +298,10 @@ void importJoinTests(const Context& context) { change(heap, piece, true); change(heap + piece, piece, true); Require(bind(reinterpret_cast(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined"); + const auto heapBase = reinterpret_cast(heap); + const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece); + const auto* pieces = HostImportFor(importing, heapBase, 2 * piece); + Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one"); const auto* kept = HostImportFor(importing, first, 16); Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); change(guest + 2 * large, large, true); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 244f7ace2c..e451ec59b8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -75,7 +75,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. -- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. +- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. From d275a58814d5c06e3a0f3100809f451daeac537a Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:13:09 +0200 Subject: [PATCH 227/353] fix(agc-driver): copy a region an exact join's import covers only in part After an exact two-piece join, a bind across the join's end got the joined import from importAllocation and the view failed as "rounded up to its view range exceeds its GPU owner". UploadFinish now asks importJoined for a region past the import's end and drops an import that still does not cover the region, so the region is copied, as on main. The join test binds across the end of an exact join after a third piece is registered. Co-authored-by: TideOPL <57710757+TideOPL@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3a58f722d0..5813a30417 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -3087,6 +3087,8 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (entry == nullptr) entry = findImport(state, region.begin, region.end); if (entry == nullptr) { if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges()); + if (entry != nullptr && region.end > entry->base + entry->bytes) entry = importJoined(context, state, region.begin, region.end, importRanges()); + if (entry != nullptr && (region.begin < entry->base || region.end > entry->base + entry->bytes)) entry = nullptr; } // A staged region (see stagingEligible) is copied out of the import even when aligned; // without a recorder to record the copies it binds in place like any other. diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 213590932a..85d5605604 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -302,6 +302,8 @@ void importJoinTests(const Context& context) { const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece); const auto* pieces = HostImportFor(importing, heapBase, 2 * piece); Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one"); + change(heap + 2 * piece, piece, true); + static_cast(bind(heapBase + 2 * piece - 16)); const auto* kept = HostImportFor(importing, first, 16); Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); change(guest + 2 * large, large, true); @@ -312,6 +314,7 @@ void importJoinTests(const Context& context) { Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other"); change(heap, piece, false); change(heap + piece, piece, false); + change(heap + 2 * piece, piece, false); change(guest + large, large, false); const auto* alone = HostImportFor(importing, first, 16); Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges"); From 4307e3cb549a5745346b39dbaf4784c4e10002f3 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 22:10:20 +0000 Subject: [PATCH 228/353] fix(agc): serialize pipeline cache snapshots --- .../Execution/src/VulkanDevice.cpp | 6 ++++-- .../Graphics/include/Context.hpp | 20 +++++++++++++++++++ .../Graphics/include/PipelineCache.hpp | 2 ++ .../Graphics/src/GpuColorTransfer.cpp | 5 ++++- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 7 ++++++- .../Graphics/src/PipelineCache.cpp | 14 ++++++++----- .../Graphics/src/PipelineLibrary.cpp | 20 +++++++++++++++---- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 15 +++++++++++--- .../Graphics/src/TextureDetiler.cpp | 11 ++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 10 ++++++++++ 10 files changed, 92 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5d3ddfec4c..4e72b41c06 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -94,7 +94,7 @@ bool CoreVulkanCapability(std::uint32_t capability) { } } -void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) { +void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) { VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; info.flags = flags; info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; @@ -107,6 +107,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, subgroup.requiredSubgroupSize = subgroupSize; if (subgroupSize != 0) info.stage.pNext = &subgroup; info.layout = layout; + auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); } @@ -3120,6 +3121,7 @@ Graphics::Context VulkanDevice::buildContext() const { state->depthClamp, state->hostImportAlignment }; + context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; context.dmaBufImport = state->dmaBufImport; context.recorder = state->recorder.get(); context.descriptorCache = state->descriptorCache.get(); @@ -4275,7 +4277,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); } if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline); + 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; Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 68ba7ea944..c83858906b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -28,6 +30,19 @@ class SamplerCache; class ShaderResources; class PlaceholderImages; +class PipelineCacheSynchronization { +public: + using PipelineUseLock = std::shared_lock; + using SnapshotLock = std::unique_lock; + + PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); } + SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); } + SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); } + +private: + mutable std::shared_mutex mutex; +}; + inline void Require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error("AGC graphics: " + reason); } @@ -176,6 +191,11 @@ struct Context { bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; VkSampleCountFlags sampleLocationSampleCounts = 0; + PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; + + PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { + return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; + } template TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 54727e9459..6398fde6d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -21,6 +21,7 @@ public: PipelineCache(const PipelineCache&) = delete; PipelineCache& operator=(const PipelineCache&) = delete; VkPipelineCache Handle() const { return cache; } + PipelineCacheSynchronization& Synchronization() { return synchronization; } private: void load(std::vector& initialData); @@ -30,6 +31,7 @@ private: Context context; VkPhysicalDeviceProperties properties{}; VkPipelineCache cache = VK_NULL_HANDLE; + PipelineCacheSynchronization synchronization; std::filesystem::path path; std::size_t savedBytes = 0; std::mutex mutex; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 4aec57b1d8..b7d132af94 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -35,7 +35,10 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = pipelineLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 6eeefefa2a..2a7abf526c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -499,13 +499,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput return; } if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); - else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + else { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + } if (alternateAlpha) { blend.pAttachments = secondChannels.data(); + auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); } if (state.depthBiasPerFace) { raster.cullMode = VK_CULL_MODE_FRONT_BIT; + auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); } for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index b3f1de78cb..ec51c5a20b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -106,11 +106,15 @@ void PipelineCache::load(std::vector& initialData) { void PipelineCache::save(bool final) { const auto getData = context.Function("vkGetPipelineCacheData"); std::size_t size = 0; - if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; - std::vector file(sizeof(FileHeader) + size); - const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); - if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; - file.resize(sizeof(FileHeader) + size); + std::vector file; + { + auto synchronizationLock = synchronization.LockForSnapshot(); + if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; + file.resize(sizeof(FileHeader) + size); + const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); + if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; + file.resize(sizeof(FileHeader) + size); + } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; std::memcpy(file.data(), &header, sizeof(header)); const auto started = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index bfdb538df6..bd2a12ed32 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -205,7 +205,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } VkPipeline handle = VK_NULL_HANDLE; if (partsOnly) lock.unlock(); - const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + } if (partsOnly) lock.lock(); Check(created, "vkCreateGraphicsPipelines library"); if (const auto raced = cache.find(key); raced != cache.end()) { @@ -234,7 +238,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); + } ++device.counters.linked; if (optimize && optimized != nullptr) { auto slot = std::make_shared(); @@ -243,8 +250,9 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip *optimized = slot; const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); const auto cache = context.pipelineCache; + const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] { + enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); @@ -253,7 +261,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; create.layout = libraryLayout; VkPipeline result = VK_NULL_HANDLE; - if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return; + const auto pipelineCreationResult = [&] { + auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; + return createPipelines(slot->device, cache, 1, &create, nullptr, &result); + }(); + if (pipelineCreationResult != VK_SUCCESS) return; slot->handle.store(result); if (slot->released.load()) { if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 192d1e9c3c..269102624b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1998,7 +1998,10 @@ bool Recorder::gpuSampleCounter() { pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = sampleLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; @@ -2009,7 +2012,10 @@ bool Recorder::gpuSampleCounter() { Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; } catch (const std::exception& error) { @@ -2286,7 +2292,10 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = meshArgumentLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); meshArgumentState = 1; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index bed858e7a1..95ed372398 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -181,7 +181,10 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem createInfo.stage = stage; createInfo.layout = image ? imagePipelineLayout : pipelineLayout; VkPipeline result = VK_NULL_HANDLE; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + } pipelines.emplace_back(key, result); return result; } @@ -299,7 +302,11 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineInfo.stage.module = cmaskModule; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = cmaskPipelineLayout; - const auto created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline); + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline); + } context.Function("vkDestroyShaderModule")(context.device, cmaskModule, nullptr); Check(created, "vkCreateComputePipelines CMASK clear"); cmaskErrors = std::make_unique(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4f1468bbb7..4589213ebe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5628,6 +5628,16 @@ int main(int argc, char** argv) { std::cout << "address-space device teardown releases cached guest allocations\n"; return 0; } + { + AgcDriver::Graphics::PipelineCacheSynchronization synchronization; + auto pipelineUse = synchronization.LockForPipelineUse(); + bool snapshotAcquired = false; + std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); }); + snapshot.join(); + Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation"); + pipelineUse.unlock(); + Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished"); + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; From d9852df2a252acd0e952640868af26a5d831ffe8 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Wed, 14 Oct 2026 00:14:55 +0200 Subject: [PATCH 229/353] fix(libc): track writes to singly mapped direct memory without faults on Windows A direct memory page mapped at one guest address now lives in private MEM_WRITE_WATCH memory at that address, and its writes are collected with GetWriteWatch like flexible memory: no fault is raised. A page mapped at a second address moves its whole resident run into the section, behind a read-only fence while it is copied, and every address becomes a view of it, tracked by write protection. Unmapped resident pages that are not blank are kept in a private reservation until they are mapped again, not in the section. Ported onto main's 4 KiB subpage tracking of shared views: resident runs keep a per-page collected bit and enter the fresh ranges, so Fresh and Armed collections select them like shared views; a demoted run carries its collected bits into the views and reports every page once. MapAlias throws through refuse() when a resident run extends outside the aliased range, and the caller keeps main's fallback. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 44 +++ .../tests/execution/DccKeysFirstWrite.cpp | 8 + core/libs/prx/libc/include/GuestArena.hpp | 3 + .../libs/prx/libc/include/WindowsMappings.hpp | 300 +++++++++++++++++- core/libs/prx/libc/src/GuestArena.cpp | 14 +- .../src/specifics/windows/CrashReport.cpp | 1 + .../libkernel/DirectMemory/DirectMemory.cpp | 36 +++ core/libs/tests/GuestMemory.cpp | 190 +++++++++++ core/libs/tests/WindowsWriteWatch.cpp | 6 +- docs/dev/TechnicalDebt.md | 2 +- docs/user/USAGE.md | 2 +- 12 files changed, 586 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4fc24a5f2b..a906fcbbeb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -985,6 +985,9 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) + set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index dd0b50ba6c..3cd2821115 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1310,6 +1310,41 @@ void hostImportRetirementTests(const Device& device) { Require(preservedEveryRecorder, "unlocked mapping changed recorder batches"); std::cout << "Host import retirement locking and GPU readback tests passed\n"; } +#ifdef _WIN32 +void secondMappingImportTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: imports after a second mapping not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + constexpr std::size_t page = 16384; + struct Release { + void* mapping = nullptr; + void* alias = nullptr; + std::int64_t physical = -1; + ~Release() { + if (alias != nullptr) sceKernelMunmap(alias, page); + if (mapping != nullptr) sceKernelMunmap(mapping, bytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes); + } + } release; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing"); + Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing"); + const auto address = reinterpret_cast(release.mapping); + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n"; + return; + } + const auto first = HostImportSerial(context, address, bytes, true); + Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import"); + Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time"); + Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page"); + const auto second = HostImportSerial(context, address, bytes, true); + Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept"); + HostImportFor(context, address, bytes); +} +#endif void movedMetadataTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); @@ -6436,6 +6471,12 @@ int main(int argc, char** argv) { return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") { + secondMappingImportTests(device); + return 0; + } +#endif readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); @@ -6466,6 +6507,9 @@ int main(int argc, char** argv) { storeRunTests(device, recorder); queuedWriteGroupTests(); remappedImportTests(device); +#ifdef _WIN32 + secondMappingImportTests(device); +#endif movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index eae20eb5a1..ceb4547674 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -100,6 +100,9 @@ public: const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); Require(section != nullptr, "cannot create shared test memory"); GuestArena::GuestArenaMap_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); + twin = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + Require(twin != nullptr, "cannot reserve the second mapping of shared test memory"); + GuestArena::GuestArenaMap_nid_postfix(twin, BlockBytes, section, 0, PAGE_READWRITE); CloseHandle(section); } else if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); } else { @@ -130,6 +133,10 @@ public: ~GuestBlock() { GuestAllocations::Mutation().Remove(block); #ifdef _WIN32 + if (twin != nullptr) { + GuestArena::GuestArenaReset_nid_postfix(twin, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(twin, BlockBytes); + } if (watched) { GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); @@ -154,6 +161,7 @@ public: private: bool watched; std::uint8_t* block = nullptr; + std::uint8_t* twin = nullptr; }; std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 5029a7717a..8a9e07a37d 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -30,6 +30,7 @@ void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); #ifdef _WIN32 bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address); +bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address); void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes); bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection); @@ -42,6 +43,8 @@ void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintp void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); +bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end); +void GuestArenaForgetSection_nid_postfix(void* section); #else void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver); void GuestArenaSetSharedBackingWriter_nid_no_patch(SharedBackingWriter writer); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 2bd449f022..08df34a85e 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -74,33 +75,67 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched) { std::lock_guard lock(mutex); auto cursor = reinterpret_cast(address); reset(cursor, bytes); + watchResident = watched; HANDLE duplicate = nullptr; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); - for (std::size_t done = 0; done < bytes; done += pageBytes) { - split(cursor + done, pageBytes); - void* page = reinterpret_cast(cursor + done); - if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page"); - if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); - DWORD previous; - if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); - const auto key = std::make_pair(reinterpret_cast(section), offset + done); - auto shared = physical[key].lock(); + const auto slotOf = [&](std::size_t done) -> Physical& { return physical[std::make_pair(reinterpret_cast(section), offset + done)]; }; + const auto pageOf = [](Physical& slot) { + auto shared = slot.page.lock(); if (!shared) { shared = std::make_shared(); - physical[key] = shared; + slot.page = shared; } + return shared; + }; + const auto single = [&](std::size_t done) { + const auto& slot = slotOf(done); + const auto shared = slot.page.lock(); + return !slot.stored && (!shared || shared->aliases.empty()); + }; + for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; + if (single(done)) { + auto runBytes = pageBytes; + while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; + split(base, runBytes); + commitResident(base, runBytes); + residentRuns.emplace(base, Run{base + runBytes, {}, static_cast(runBytes >> 12)}); + for (std::size_t at = 0; at < runBytes; at += pageBytes) { + auto& slot = slotOf(done + at); + const auto shared = pageOf(slot); + if (slot.saved) { + std::memcpy(reinterpret_cast(base + at), slot.saved->bytes + slot.savedOffset, pageBytes); + slot.saved.reset(); + } + shared->aliases.push_back(base + at); + views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, &slot}); + invalidate(*shared); + } + remember(freshRanges, base, base + runBytes); + DWORD previous; + if (protection != PAGE_READWRITE && !VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect resident guest pages"); + done += runBytes; + continue; + } + auto& slot = slotOf(done); + const auto shared = pageOf(slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + split(base, pageBytes); + mapShared(base, section, offset + done, protection); + slot.stored = true; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, &slot}); remember(freshRanges, base, base + pageBytes); invalidate(*shared); + done += pageBytes; } } @@ -143,6 +178,10 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; + if (view.resident) { + const auto memory = query(address); + return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); + } const auto written = subpagesOf(base, address, address + 1); invalidate(*view.page, written); protectSubpages(base, written, view.protection, "resume shared memory write"); @@ -150,6 +189,36 @@ public: return true; } + bool HandleRead(std::uintptr_t address) { + std::lock_guard lock(mutex); + if (!views.contains(address & ~(pageBytes - 1))) return false; + const auto memory = query(address); + return memory.State == MEM_COMMIT && (memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; + } + + bool ResidentSpan(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(mutex); + auto run = residentRuns.upper_bound(address); + if (run != residentRuns.begin() && std::prev(run)->second.end > address) --run; + if (run == residentRuns.end() || run->first >= address + bytes) return false; + *start = run->first; + for (; run != residentRuns.end() && run->first < address + bytes; ++run) *end = run->second.end; + return true; + } + + void ForgetSection(HANDLE section) { + std::lock_guard lock(mutex); + const auto key = reinterpret_cast(section); + for (auto slot = physical.lower_bound(std::make_pair(key, std::uint64_t{0})); slot != physical.end() && slot->first.first == key;) { + const auto shared = slot->second.page.lock(); + if (shared && !shared->aliases.empty()) { + ++slot; + continue; + } + slot = physical.erase(slot); + } + } + bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); @@ -187,6 +256,16 @@ public: return std::runtime_error(text); }; if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) throw refuse("the range is not made of whole shared pages"); + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto found = views.find(address + done); + if (found == views.end() || !found->second.resident) continue; + const auto run = std::prev(residentRuns.upper_bound(address + done)); + if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range"); + } + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto found = views.find(address + done); + if (found != views.end() && found->second.resident) demote(address + done); + } auto view = views.find(address); if (view == views.end()) throw refuse("the range does not start at a shared view"); const auto section = view->second.section; @@ -244,6 +323,10 @@ public: const auto fresh = freshRanges.upper_bound(address); if ((fresh == freshRanges.begin() || std::prev(fresh)->second <= address) && (fresh == freshRanges.end() || fresh->first >= end)) return true; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { + if (it->second.resident) { + if (!watchResident) return false; + continue; + } if (it->second.protection != PAGE_NOACCESS) continue; for (auto at = std::max(address, it->first); at < std::min(end, it->first + pageBytes); at += 4096) { if ((it->second.collected & (1u << ((at - it->first) / 4096))) == 0) return false; @@ -261,7 +344,16 @@ public: } const auto base = cursor & ~(pageBytes - 1); const auto found = views.find(base); - if (found != views.end()) { + if (found != views.end() && found->second.resident) { + if (!watchResident) return false; + const auto available = capacity - *count; + if (available == 0) return true; + const auto run = std::prev(residentRuns.upper_bound(cursor)); + const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096}); + collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection); + if (*count == capacity) return true; + cursor = stop; + } else if (found != views.end()) { auto& view = found->second; auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); if (collection != Collection::All) { @@ -309,6 +401,7 @@ public: std::uint8_t untracked = 0; for (const auto alias : view.page->aliases) { auto& other = views.at(alias); + if (other.resident) continue; const auto arming = static_cast(settled & ~other.armed); if (!writable(other.protection) || arming == 0) continue; if (other.hostWrites != 0) { @@ -364,7 +457,7 @@ private: for (auto cursor = address; cursor < end;) { const auto base = cursor & ~(pageBytes - 1); if (const auto found = views.find(base); found != views.end()) { - if (found->second.protection == PAGE_NOACCESS) return false; + if (found->second.protection == PAGE_NOACCESS || (found->second.resident && !watchResident)) return false; cursor = std::min(end, base + pageBytes); continue; } @@ -380,6 +473,7 @@ private: static_assert(subpageBytes == 4096, "Collect reports one page per subpage"); static constexpr std::size_t subpages = pageBytes / subpageBytes; static constexpr std::uint8_t allSubpages = (1u << subpages) - 1; + static constexpr std::size_t chunkBytes = 0x200000; struct SharedPage { std::array generation = [] { std::array initial{}; @@ -396,6 +490,19 @@ private: Section& operator=(const Section&) = delete; ~Section() { CloseHandle(handle); } }; + struct Saved { + unsigned char* bytes; + explicit Saved(unsigned char* bytes) : bytes(bytes) {} + Saved(const Saved&) = delete; + Saved& operator=(const Saved&) = delete; + ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + }; + struct Physical { + std::weak_ptr page; + bool stored = false; + std::shared_ptr saved; + std::size_t savedOffset = 0; + }; struct View { std::shared_ptr page; DWORD protection; @@ -405,9 +512,159 @@ private: std::uint64_t offset; std::uint32_t hostWrites; std::uint8_t collected = 0; + bool resident = false; + Physical* slot = nullptr; + }; + struct Run { + std::uintptr_t end; + std::array collected; + std::uint32_t fresh; + }; + struct Window { + void* view = nullptr; + unsigned char* bytes = nullptr; }; using Ranges = std::map; + static bool runCollected(const Run& run, std::size_t bit) { + return (run.collected[bit >> 6] >> (bit & 63) & 1) != 0; + } + + void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { + const auto first = from & ~std::uintptr_t{4095}; + std::vector written; + if (collection != Collection::Fresh) { + std::vector found((stop - first + 4095) / 4096); + ULONG_PTR reported = found.size(); + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, found.data(), &reported, &granularity) != 0) fail("collect resident guest writes"); + for (ULONG_PTR index = 0; index < reported; ++index) written.push_back(reinterpret_cast(found[index])); + } + auto next = written.begin(); + std::uintptr_t resetFrom = 0; + const auto flushReset = [&](std::uintptr_t to) { + if (resetFrom != 0 && ResetWriteWatch(reinterpret_cast(resetFrom), to - resetFrom) != 0) fail("reset resident guest writes"); + resetFrom = 0; + }; + for (auto at = first; at < stop; at += 4096) { + const auto bit = (at - start) >> 12; + const bool fresh = !runCollected(run, bit); + bool wrote = false; + while (next != written.end() && *next < at) ++next; + if (next != written.end() && *next == at) wrote = true; + const bool report = collection == Collection::Fresh ? fresh : collection == Collection::Armed ? !fresh && wrote : fresh || wrote; + if (report) pages[(*count)++] = reinterpret_cast(std::max(at, from)); + const bool settle = clear && fresh && collection != Collection::Armed; + if (settle) { + run.collected[bit >> 6] |= 1ull << (bit & 63); + --run.fresh; + forget(freshRanges, at, at + 4096); + if (collection == Collection::Fresh && resetFrom == 0) resetFrom = at; + } else if (collection == Collection::Fresh) { + flushReset(at); + } + } + if (collection == Collection::Fresh) flushReset(stop); + } + + Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { + const auto lead = offset % granularity; + Window window; + window.view = map(section, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); + if (window.view == nullptr) fail("open a window on direct memory"); + window.bytes = static_cast(window.view) + lead; + return window; + } + + void close(Window& window) { + if (window.view != nullptr && !unmap(GetCurrentProcess(), window.view, 0)) fail("close a window on direct memory"); + window = {}; + } + + void commitResident(std::uintptr_t base, std::size_t bytes) { + const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watchResident ? MEM_WRITE_WATCH : 0); + if (!allocate(GetCurrentProcess(), reinterpret_cast(base), bytes, flags, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder with resident direct memory"); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) { + void* page = reinterpret_cast(base); + if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + if (!VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + DWORD previous; + if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + } + + static bool blank(std::uintptr_t page) { + const auto* words = reinterpret_cast(page); + for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { + if (words[i] != 0) return false; + } + return true; + } + + void demote(std::uintptr_t address) { + const auto run = std::prev(residentRuns.upper_bound(address)); + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages"); + const auto section = views.at(start).section; + auto window = open(section->handle, views.at(start).offset, stop - start); + if (!VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); + std::memcpy(window.bytes, reinterpret_cast(start), stop - start); + close(window); + if (!VirtualFree(reinterpret_cast(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages"); + for (auto page = start; page < stop; page += pageBytes) { + auto& view = views.at(page); + split(page, pageBytes); + mapShared(page, view.section->handle, view.offset, view.protection); + view.slot->stored = true; + view.resident = false; + view.seen.fill(0); + view.armed = 0; + view.collected = 0; + for (std::size_t sub = 0; sub < subpages; ++sub) { + if (runCollected(run->second, ((page - start) >> 12) + sub)) view.collected |= static_cast(1u << sub); + } + invalidate(*view.page); + } + residentRuns.erase(run); + } + + void breakRun(std::uintptr_t at) { + const auto next = residentRuns.upper_bound(at); + if (next == residentRuns.begin()) return; + const auto run = std::prev(next); + if (at > run->first && at < run->second.end) demote(at); + } + + void evict(std::map::iterator run) { + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("open resident guest pages"); + std::shared_ptr saved; + for (auto page = start; page < stop; page += pageBytes) { + const auto found = views.find(page); + auto& view = found->second; + if (!blank(page)) { + if (!saved) { + auto* bytes = static_cast(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE)); + if (bytes == nullptr) fail("reserve unmapped direct memory"); + saved = std::make_shared(bytes); + } + if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); + view.slot->saved = saved; + view.slot->savedOffset = page - start; + } + std::erase(view.page->aliases, page); + views.erase(found); + } + forget(freshRanges, start, stop); + residentRuns.erase(run); + } + static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { auto it = ranges.lower_bound(start); if (it != ranges.begin() && std::prev(it)->second > start) --it; @@ -490,6 +747,9 @@ private: unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); compare = reinterpret_cast(GetProcAddress(module, "CompareObjectHandles")); + SYSTEM_INFO system{}; + GetSystemInfo(&system); + granularity = system.dwAllocationGranularity; } [[noreturn]] static void fail(const char* operation) { @@ -518,6 +778,8 @@ private: void reset(std::uintptr_t address, std::size_t bytes) { const auto end = address + bytes; forgetClean(address, end); + breakRun(address); + breakRun(end); for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -541,6 +803,7 @@ private: forget(freshRanges, cursor, cursor + pageBytes); } } else if (memory.Type == MEM_PRIVATE) { + if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); } else { throw std::runtime_error("unsupported guest mapping type"); @@ -594,8 +857,11 @@ private: Ranges cleanRanges; Ranges freshRanges; std::map views; - std::map, std::weak_ptr> physical; + std::map, Physical> physical; + std::map residentRuns; std::map splitAliases; + std::size_t granularity = 0x10000; + bool watchResident = false; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5ff89ebc9c..44df388a33 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -320,6 +320,10 @@ bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) { return WindowsMappings::Get().HandleWrite(address); } +bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address) { + return WindowsMappings::Get().HandleRead(address); +} + void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes) { if (!Arena::Get().Contains(pointer, bytes)) return; WindowsMappings::Get().Pin(reinterpret_cast(pointer), bytes); @@ -374,7 +378,7 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched()); } void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) { @@ -384,6 +388,14 @@ void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) void GuestArenaUnmapAlias_nid_postfix(void* alias) { WindowsMappings::Get().UnmapAlias(alias); } + +bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + return WindowsMappings::Get().ResidentSpan(address, bytes, start, end); +} + +void GuestArenaForgetSection_nid_postfix(void* section) { + WindowsMappings::Get().ForgetSection(section); +} #endif #if defined(__linux__) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index a4671a43eb..1c69a3d3a6 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -246,6 +246,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; const auto* fault = info->ExceptionRecord; if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; + if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 0 && GuestArena::GuestArenaHandleRead_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index a35b55b556..cba67b8f5a 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -302,6 +302,7 @@ public: ~PhysicalBacking() { #ifdef _WIN32 + GuestArena::GuestArenaForgetSection_nid_postfix(section); CloseHandle(section); #else if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes); @@ -552,11 +553,44 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int #endif } +void RequireResidentUnpinned(GuestAllocations::Mutation& mutation, std::uintptr_t address, std::size_t len) { +#ifdef _WIN32 + std::uintptr_t start = 0; + std::uintptr_t end = 0; + if (GuestArena::GuestArenaResidentSpan_nid_postfix(address, len, &start, &end)) mutation.RequireUnpinned(reinterpret_cast(start), end - start); +#else + static_cast(mutation); + static_cast(address); + static_cast(len); +#endif +} + +void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t phys, std::size_t len) { +#ifdef _WIN32 + std::vector> aliases; + { + std::lock_guard lock(g_directLock); + for (const auto& [base, mapping] : g_directMappings) { + const auto first = std::max(phys, mapping.phys); + const auto last = std::min(phys + len, mapping.phys + (mapping.end - base)); + if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first); + } + } + for (const auto& [address, bytes] : aliases) RequireResidentUnpinned(mutation, address, bytes); +#else + static_cast(mutation); + static_cast(phys); + static_cast(len); +#endif +} + bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; if (addr == nullptr || (flags & GuestMapFixed) == 0 || (flags & GuestMapNoOverwrite) != 0 || !mutation.Covers(addr, len)) return false; ValidateRange(addr, len, PS5_PAGE_SIZE); + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); + if (physStart >= 0) RequireAliasesUnpinned(mutation, static_cast(physStart), len); Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast(physStart)); const auto nativeProtection = LinuxProtFromSce(prot); mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { @@ -798,6 +832,7 @@ bool Reserved(const void* addr, size_t len) { } void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len) { + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); mutation.Unmap(addr, len, [&](const void* piece, std::size_t pieceBytes, const void* allocation, bool last) { auto* pieceAddress = const_cast(piece); std::lock_guard lock(g_directLock); @@ -847,6 +882,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return 0; } ReplaceFixedOverlap(mutation, *addr, len, flags); + RequireAliasesUnpinned(mutation, static_cast(physStart), len); std::lock_guard lock(g_directLock); ValidatePhysicalRange(static_cast(physStart), len); void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 5c0916db99..c1a9f6484f 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -37,6 +37,7 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include +#include #endif extern "C" { @@ -1318,6 +1319,188 @@ static void CheckHeapAfterMappingReuse() { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +#ifdef _WIN32 +static int g_accessViolations = 0; + +static LONG WINAPI CountAccessViolations(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) ++g_accessViolations; + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + +static void CheckSingleDirectMappingTracksWritesWithoutFaults() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + const auto collect = [](void* address, std::size_t bytes) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, true)); + return count; + }; + auto* bytes = static_cast(mapped); + Require(collect(mapped, page * 4) == 16); + Require(collect(mapped, page * 4) == 0); + void* const handler = AddVectoredExceptionHandler(1, CountAccessViolations); + Require(handler != nullptr); + bytes[page + 0x1000] = 7; + bytes[page * 3] = 9; + Require(RemoveVectoredExceptionHandler(handler) != 0); + Require(g_accessViolations == 0); + Require(collect(mapped, page * 4) == 2); + Require(collect(mapped, page * 4) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 2, page) == 0); + Require(bytes[page + 0x1000] == 7 && bytes[page * 3] == 9); + bytes[0] = 5; + Require(collect(mapped, page * 2) != 0); + Require(collect(mapped, page * 2) == 0); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(static_cast(second)[0x1000] == 7); + static_cast(second)[0x1001] = 11; + Require(bytes[page + 0x1001] == 11); + Require(collect(mapped, page * 2) != 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 3, page) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page * 4, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 5 && data[page + 0x1000] == 7 && data[page + 0x1001] == 11 && data[page * 2] == 0 && data[page * 3] == 9); + Require(sceKernelMunmap(again, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +static void CheckSecondDirectMappingWaitsForLeases() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast(mapped); }) == 1); + std::atomic released{false}; + std::thread holder([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + released = true; + lease.clear(); + }); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(released); + holder.join(); + static_cast(second)[8] = 3; + Require(static_cast(mapped)[page + 8] == 3); + static_cast(mapped)[page * 2 + 8] = 4; + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +static void CheckBreakingAResidentRunKeepsConcurrentAccesses() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t chunk = 0x200000; + constexpr std::size_t chunks = 4; + for (int round = 0; round < 20; ++round) { + const bool unmap = round % 2 != 0; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, chunk * chunks, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, chunk * chunks, 3, 0, phys, 0) == 0); + auto* memory = static_cast(mapped); + const auto value = [&](std::size_t at) { return static_cast((at * 31 + static_cast(round)) | 1); }; + const auto skipped = [&](std::size_t offset) { return unmap && offset >= page && offset < page * 2; }; + std::atomic go{false}; + std::atomic done{false}; + std::atomic consistent{true}; + std::thread writer([&] { + while (!go) {} + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) memory[index * chunk + offset] = value(index * chunk + offset); + } + }); + std::thread reader([&] { + while (!go) {} + while (!done) { + for (std::size_t offset = 0; offset < chunk && !done; offset += 61) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) { + const unsigned char seen = memory[index * chunk + offset]; + if (seen != 0 && seen != value(index * chunk + offset)) consistent = false; + } + } + } + }); + std::array second{}; + go = true; + for (std::size_t index = 0; index < chunks; ++index) { + if (unmap) Require(sceKernelMunmap(static_cast(mapped) + index * chunk + page, page) == 0); + else Require(sceKernelMapDirectMemory(&second[index], page, 3, 0, phys + static_cast(index * chunk + page), 0) == 0); + } + writer.join(); + done = true; + reader.join(); + Require(consistent); + for (std::size_t index = 0; index < chunks; ++index) { + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (!skipped(offset)) Require(memory[index * chunk + offset] == value(index * chunk + offset)); + } + if (unmap) continue; + for (std::size_t offset = 0; offset < page; ++offset) Require(static_cast(second[index])[offset] == value(index * chunk + page + offset)); + Require(sceKernelMunmap(second[index], page) == 0); + } + for (std::size_t index = 0; index < chunks; ++index) { + auto* base = static_cast(mapped) + index * chunk; + if (!unmap) { + Require(sceKernelMunmap(base, chunk) == 0); + continue; + } + Require(sceKernelMunmap(base, page) == 0); + Require(sceKernelMunmap(base + page * 2, chunk - page * 2) == 0); + } + Require(sceKernelReleaseDirectMemory(phys, chunk * chunks) == 0); + } +#endif +} + +static void CheckRemappedDirectMemoryIsChargedOnce() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x10000000; + const auto committed = [] { + PERFORMANCE_INFORMATION info{}; + info.cb = sizeof(info); + Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0); + return static_cast(info.CommitTotal) * static_cast(info.PageSize); + }; + bool once = false; + for (int attempt = 0; attempt < 3 && !once; ++attempt) { + const auto before = committed(); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + std::memset(mapped, 0xa5, bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); + once = committed() - before < static_cast(bytes + bytes / 2); + Require(sceKernelMunmap(again, bytes) == 0); + Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); + } + Require(once); +#endif +} + static void CheckSharedWriteTracking() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -1535,7 +1718,9 @@ static void CheckPinnedSharedPages() { std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); void* mapped = nullptr; + void* alias = nullptr; Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&alias, page * 3, 3, 0, phys, 0) == 0); auto* bytes = static_cast(mapped); const auto collect = [&] { std::array pages{}; @@ -1567,6 +1752,7 @@ static void CheckPinnedSharedPages() { Require(protection(page) == PAGE_READONLY); bytes[page + 8] = 11; Require(collect() == 1); + Require(sceKernelMunmap(alias, page * 3) == 0); Require(sceKernelMunmap(mapped, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); #endif @@ -2229,6 +2415,10 @@ int main() { CheckReadsIntoSharedWriteTracking(); CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); + CheckSingleDirectMappingTracksWritesWithoutFaults(); + CheckSecondDirectMappingWaitsForLeases(); + CheckBreakingAResidentRunKeepsConcurrentAccesses(); + CheckRemappedDirectMemoryIsChargedOnce(); #if defined(__linux__) CheckWriteWatch(); CheckWriteWatchArmedCollect(); diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 08b6d4ea12..f6e799bf63 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -32,8 +32,8 @@ struct Region { Region() { Require(section && first && alias, "cannot allocate shared test memory"); - Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE); - Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE); + Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE, true); + Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE, true); } ~Region() { Mappings::Get().Reset(first, Bytes); @@ -113,7 +113,7 @@ void CheckAliasesAndCapacity() { Require(region.Collect(Collection::Armed) == Pages(0, 1), "alias write was missed or fresh neighbors were consumed"); Require(region.Collect(Collection::Fresh) == Pages(1, 16), "alias collection changed another view's history"); Mappings::Get().Reset(region.first, Bytes); - Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE); + Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE, true); Require(region.Collect(Collection::Armed).empty(), "remapped address kept old collection history"); Require(region.Collect(Collection::Fresh) == Pages(0, 16), "remapped memory was not fresh"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1a7c6bbe89..ef7bd01664 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -13,7 +13,7 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Direct memory -- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. +- 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. ### Silent stubs diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4e75ad5559..a404a79fa9 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -97,7 +97,7 @@ Use the generated files printed as `Guest module:` for bundled title modules. `l On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. -On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed in full when the title allocates it, not when its pages are first used. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover it together with all other committed memory. Otherwise the allocation throws `create direct memory backing of 0x bytes ( MiB)` with the Windows error; enlarge the page file or close other applications. +On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. Ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. From 71da166a1808033e977eba6581e81d5ec6a41009 Mon Sep 17 00:00:00 2001 From: thibaut Date: Wed, 14 Oct 2026 00:24:09 +0200 Subject: [PATCH 230/353] fix(libc): keep resident mappings to direct memory backings on Windows Map made every singly mapped section page private resident memory. A JIT or shm_open section is file backed and committed: its bytes live in the file, and its alias descriptors map duplicated handles, which the physical page slots do not match, so a resident copy diverged from the file, and committing its view on a demote failed with access denied (guest_jit_shared_memory). Only direct memory backings, mapped through the new GuestArenaMapDirect, now become resident; other sections keep main's shared views. Shared pages are committed only while their section page is reserved, as on main. The socket host write tests map their direct memory a second time, so the write-protected shared path they check is still the one they exercise. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- core/libs/prx/libc/include/GuestArena.hpp | 1 + core/libs/prx/libc/include/WindowsMappings.hpp | 10 +++++----- core/libs/prx/libc/src/GuestArena.cpp | 7 ++++++- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 2 +- core/libs/tests/GuestSocketHostWrite.cpp | 7 +++++++ core/libs/tests/WindowsWriteWatch.cpp | 6 +++--- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 8a9e07a37d..3c96845e4d 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -41,6 +41,7 @@ bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_ std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 08df34a85e..87f9f78308 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -75,11 +75,11 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) { std::lock_guard lock(mutex); auto cursor = reinterpret_cast(address); reset(cursor, bytes); - watchResident = watched; + if (resident) watchResident = watched; HANDLE duplicate = nullptr; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); @@ -95,7 +95,7 @@ public: const auto single = [&](std::size_t done) { const auto& slot = slotOf(done); const auto shared = slot.page.lock(); - return !slot.stored && (!shared || shared->aliases.empty()); + return resident && !slot.stored && (!shared || shared->aliases.empty()); }; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; @@ -589,7 +589,7 @@ private: void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) { void* page = reinterpret_cast(base); if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - if (!VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); } @@ -610,7 +610,7 @@ private: if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages"); const auto section = views.at(start).section; auto window = open(section->handle, views.at(start).offset, stop - start); - if (!VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); + if (query(reinterpret_cast(window.bytes)).State == MEM_RESERVE && !VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); std::memcpy(window.bytes, reinterpret_cast(start), stop - start); close(window); if (!VirtualFree(reinterpret_cast(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages"); diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 44df388a33..5387ae0390 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -378,7 +378,12 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched()); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false); +} + +void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true); } void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) { diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index cba67b8f5a..4b7e9a094f 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -314,7 +314,7 @@ public: void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { #ifdef _WIN32 - GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); + GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); #else if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); #endif diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index ff2b0b5d27..06e23b986e 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -141,6 +141,8 @@ int main(int argc, char** argv) { Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); void* mapping = nullptr; Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + void* twin = nullptr; + Require(sceKernelMapDirectMemory(&twin, page * 2, 3, 0, physical, 0) == 0, "map read/write memory a second time"); auto* buffer = static_cast(mapping) + page - 7; const auto collect = [&] { std::array pages{}; @@ -199,6 +201,9 @@ int main(int argc, char** argv) { void* outputMapping = nullptr; Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, "map sender output memory"); + void* outputTwin = nullptr; + Require(sceKernelMapDirectMemory(&outputTwin, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory a second time"); const auto collectOutputs = [&] { std::array pages{}; std::size_t count = pages.size(); @@ -238,6 +243,7 @@ int main(int argc, char** argv) { if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); } + Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping"); Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); } @@ -300,6 +306,7 @@ int main(int argc, char** argv) { Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(twin, page * 2) == 0, "unmap the second direct memory mapping"); Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); } diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index f6e799bf63..08b6d4ea12 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -32,8 +32,8 @@ struct Region { Region() { Require(section && first && alias, "cannot allocate shared test memory"); - Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE, true); - Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE, true); + Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE); + Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE); } ~Region() { Mappings::Get().Reset(first, Bytes); @@ -113,7 +113,7 @@ void CheckAliasesAndCapacity() { Require(region.Collect(Collection::Armed) == Pages(0, 1), "alias write was missed or fresh neighbors were consumed"); Require(region.Collect(Collection::Fresh) == Pages(1, 16), "alias collection changed another view's history"); Mappings::Get().Reset(region.first, Bytes); - Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE, true); + Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE); Require(region.Collect(Collection::Armed).empty(), "remapped address kept old collection history"); Require(region.Collect(Collection::Fresh) == Pages(0, 16), "remapped memory was not fresh"); } From f32a9407e439592dd7be6d383a06ff5b5e377d60 Mon Sep 17 00:00:00 2001 From: thibaut Date: Wed, 14 Oct 2026 00:24:09 +0200 Subject: [PATCH 231/353] test(libc): require resident direct memory in the resident run tests CheckBreakingAResidentRunKeepsConcurrentAccesses and CheckRemappedDirectMemoryIsChargedOnce passed on main, where a singly mapped direct memory page is a shared view and no resident run exists. Both now require their first mapping to be private committed memory, so on main they fail there instead of passing without exercising a run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- core/libs/tests/GuestMemory.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index c1a9f6484f..f480b59ad3 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1403,6 +1403,14 @@ static void CheckSecondDirectMappingWaitsForLeases() { #endif } +#ifdef _WIN32 +static void RequireResidentMapping(const void* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info)); + Require(info.State == MEM_COMMIT && info.Type == MEM_PRIVATE); +} +#endif + static void CheckBreakingAResidentRunKeepsConcurrentAccesses() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -1414,6 +1422,7 @@ static void CheckBreakingAResidentRunKeepsConcurrentAccesses() { Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, chunk * chunks, 0, 0, &phys) == 0); void* mapped = nullptr; Require(sceKernelMapDirectMemory(&mapped, chunk * chunks, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); auto* memory = static_cast(mapped); const auto value = [&](std::size_t at) { return static_cast((at * 31 + static_cast(round)) | 1); }; const auto skipped = [&](std::size_t offset) { return unmap && offset >= page && offset < page * 2; }; @@ -1487,6 +1496,7 @@ static void CheckRemappedDirectMemoryIsChargedOnce() { Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); void* mapped = nullptr; Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); std::memset(mapped, 0xa5, bytes); Require(sceKernelMunmap(mapped, bytes) == 0); void* again = nullptr; From b7d7e71bf6a5aeaa3c1f824cdf7b0d6c9175fafe Mon Sep 17 00:00:00 2001 From: thibaut Date: Wed, 14 Oct 2026 00:24:53 +0200 Subject: [PATCH 232/353] docs(libc): move the Windows direct memory debt entry to libc.md The entry links WindowsMappings.hpp, so the debt-file convention puts it in docs/dev/debt/libc.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- docs/dev/TechnicalDebt.md | 4 ---- docs/dev/debt/libc.md | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ef7bd01664..9119b5f114 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -11,10 +11,6 @@ Entries about one library or component are in [debt](debt/), one file per librar - libstdc++-6.dll - libwinpthread-1.dll -### Direct memory - -- 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. - ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0a6b4a8324..c1bb0932f7 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -28,6 +28,7 @@ ### Functional +- 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](../../../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. - [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 From 434e0b9edfd47fe9281f908b2db6ab273c5a7a4e Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:49:16 +0200 Subject: [PATCH 233/353] fix(libkernel): forget unwind images only when their module is unloaded and read tables outside the lock Co-Authored-By: Claude Sonnet 5.5 --- .../libkernel/Module/src/DynamicLoader.cpp | 4 +- core/libs/prx/libkernel/Module/src/Module.cpp | 61 ++++++++++++------- core/libs/prx/libkernel/System/src/Rtld.cpp | 4 +- core/libs/tests/GuestUnwindModuleInfo.cpp | 3 - docs/dev/debt/libkernel.md | 2 +- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index b66319ac67..0c1645b880 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -25,7 +25,7 @@ #endif #ifdef _WIN32 -extern "C" void ForgetUnwindImage_nid_no_patch(const void* base); +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); #endif namespace { @@ -43,7 +43,7 @@ struct Module { if (owned && native) { #ifdef _WIN32 FreeLibrary(static_cast(native)); - ForgetUnwindImage_nid_no_patch(native); + ForgetUnloadedUnwindImages_nid_no_patch(); #else ::dlclose(native); #endif diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 64d0d624ad..d87507dbff 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" @@ -65,34 +66,35 @@ void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleI info->name[sizeof(info->name) - 1] = '\0'; } -const UnwindImage& CurrentUnwindImage(void* allocationBase, const IMAGE_NT_HEADERS64& nt) { - const auto* base = static_cast(allocationBase); - auto& image = UnwindImages()[reinterpret_cast(base)]; - if (!UnwindImageCurrent(image, nt)) { - UnwindImage fresh; - fresh.size = nt.OptionalHeader.SizeOfImage; - fresh.hasTables = EhFrame::ReadEhmeta(reinterpret_cast(base), nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); - if (fresh.hasTables) { - fresh.headerWord = ReadWord(fresh.tables.header); - fresh.framesWord = ReadWord(fresh.tables.frames); - } - char path[4096] = {}; - const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); - fresh.name.assign(path, length); - fresh.filled = true; - image = std::move(fresh); - } - return image; -} - void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { const auto* base = static_cast(allocationBase); const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); + const auto key = reinterpret_cast(base); + { + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(key); + if (found != UnwindImages().end() && UnwindImageCurrent(found->second, *nt)) { + WriteUnwindInfo(found->second, base, info); + return; + } + } + UnwindImage fresh; + fresh.size = nt->OptionalHeader.SizeOfImage; + fresh.hasTables = EhFrame::ReadEhmeta(key, *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); + if (fresh.hasTables) { + fresh.headerWord = ReadWord(fresh.tables.header); + fresh.framesWord = ReadWord(fresh.tables.frames); + } + char path[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); + fresh.name.assign(path, length); + fresh.filled = true; std::lock_guard lock(unwindImagesMutex); - WriteUnwindInfo(CurrentUnwindImage(allocationBase, *nt), base, info); + const auto& image = UnwindImages().insert_or_assign(key, std::move(fresh)).first->second; + WriteUnwindInfo(image, base, info); } bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { @@ -112,9 +114,22 @@ bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { } } -extern "C" void ForgetUnwindImage_nid_no_patch(const void* base) { +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { + std::vector bases; + { + std::lock_guard lock(unwindImagesMutex); + for (const auto& image : UnwindImages()) bases.push_back(image.first); + } + std::vector unloaded; + for (const auto base : bases) { + HMODULE loaded = nullptr; + const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && + reinterpret_cast(loaded) == base; + if (!present) unloaded.push_back(base); + } + if (unloaded.empty()) return; std::lock_guard lock(unwindImagesMutex); - UnwindImages().erase(reinterpret_cast(base)); + for (const auto base : unloaded) UnwindImages().erase(base); } #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 5a4977754f..90472da544 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -20,7 +20,7 @@ #endif #ifdef _WIN32 -extern "C" void ForgetUnwindImage_nid_no_patch(const void* base); +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); #endif namespace { @@ -74,7 +74,7 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { if (!handle) return 0; #ifdef _WIN32 const bool released = FreeLibrary(static_cast(handle)); - ForgetUnwindImage_nid_no_patch(handle); + ForgetUnloadedUnwindImages_nid_no_patch(); if (!released) #else if (dlclose(handle) != 0) diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index f8479234b2..e96ebc57e1 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -35,7 +35,6 @@ static void ReloadedModuleIsRead() { std::filesystem::copy_file(original, copy, std::filesystem::copy_options::overwrite_existing); const auto path = copy.string(); - std::uint64_t firstBase = 0; for (int round = 0; round < 2; ++round) { void* handle = dlopen_nid_postfix(path.c_str(), 2); Require(handle != nullptr); @@ -46,8 +45,6 @@ static void ReloadedModuleIsRead() { MEMORY_BASIC_INFORMATION memory{}; Require(VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != 0); const auto base = reinterpret_cast(memory.AllocationBase); - if (round == 0) firstBase = base; - else Require(base == firstBase); if (round == 0) { fixture->frames.terminator = 12; fixture->frames.spare[0] = 1; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index bfc94bbe7b..3e38be7a72 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,7 +30,7 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows -- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped when the loader frees the module (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release). A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it From 05bf752bfae0c92aca92b71169b87c2e73c503b0 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:57:39 +0200 Subject: [PATCH 234/353] fix(libkernel): clear the unwind cache when forgetting unloaded images fails Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Module/src/Module.cpp | 31 +++++++++++-------- core/libs/tests/GuestUnwindModuleInfo.cpp | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index d87507dbff..b82436b90b 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -115,21 +115,26 @@ bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { } extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { - std::vector bases; - { + try { + std::vector bases; + { + std::lock_guard lock(unwindImagesMutex); + for (const auto& image : UnwindImages()) bases.push_back(image.first); + } + std::vector unloaded; + for (const auto base : bases) { + HMODULE loaded = nullptr; + const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && + reinterpret_cast(loaded) == base; + if (!present) unloaded.push_back(base); + } + if (unloaded.empty()) return; std::lock_guard lock(unwindImagesMutex); - for (const auto& image : UnwindImages()) bases.push_back(image.first); + for (const auto base : unloaded) UnwindImages().erase(base); + } catch (...) { + std::lock_guard lock(unwindImagesMutex); + UnwindImages().clear(); } - std::vector unloaded; - for (const auto base : bases) { - HMODULE loaded = nullptr; - const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && - reinterpret_cast(loaded) == base; - if (!present) unloaded.push_back(base); - } - if (unloaded.empty()) return; - std::lock_guard lock(unwindImagesMutex); - for (const auto base : unloaded) UnwindImages().erase(base); } #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index e96ebc57e1..70019db3cd 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -78,7 +78,7 @@ int main() { Require(info.seg0_size == nt->OptionalHeader.SizeOfImage); char mapped[4096] = {}; Require(GetMappedFileNameA(GetCurrentProcess(), memory.BaseAddress, mapped, sizeof(mapped) - 1) != 0); - Require(std::strcmp(info.name, mapped) == 0); + Require(std::strncmp(info.name, mapped, sizeof(info.name) - 1) == 0); for (const auto* inside : {static_cast(&fixture->header), static_cast(&fixture->frames)}) { ModuleInfoForUnwind other{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(inside), 0, &other) == 0); From 394c5a231e48cadf3ba15da3e9b97d107f3d2e89 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 15:10:28 -0700 Subject: [PATCH 235/353] fix(relinker): validate Linux guest runpaths with target syntax --- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../relinker/tests/test_guest_runpath.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index de2662f6ad..1043171b6c 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -285,7 +285,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + 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; for (const auto index : order) { diff --git a/core/relinker/relinker/tests/test_guest_runpath.py b/core/relinker/relinker/tests/test_guest_runpath.py index 754fee90a6..e5238ab66f 100644 --- a/core/relinker/relinker/tests/test_guest_runpath.py +++ b/core/relinker/relinker/tests/test_guest_runpath.py @@ -17,7 +17,8 @@ def main(): checked = 0 for index, module_directory in enumerate(("sce_module", "prx")): for label, run_path in (("default", None), ("origin", "$ORIGIN"), - ("custom", "$ORIGIN/host libs"), ("absolute", str(work / "absolute libs")), + ("custom", "$ORIGIN/host libs"), ("absolute", "/opt/absolute libs"), + ("absolute-root", "/"), ("absolute-list", "/opt/libs:/usr/local/lib"), ("list", "$ORIGIN/first:$ORIGIN/second"), ("list-bare", "$ORIGIN:$ORIGIN/libs:$ORIGIN")): case = work / f"{index}-{label}" @@ -43,13 +44,21 @@ def main(): assert 0 <= offset < len(strings) and end >= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - configured_paths = configured.split(":") if label.startswith("list") else [configured] - actual_paths = actual.split(":") if label.startswith("list") else [actual] + path_list = label.startswith("list") or label == "absolute-list" + configured_paths = configured.split(":") if path_list else [configured] + actual_paths = actual.split(":") if path_list else [actual] expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] - if resolved != expected or (label == "absolute" and actual != run_path): + if resolved != expected or (label.startswith("absolute") and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 + for index, run_path in enumerate(("relative/libs", "C:/host/libs", "C:\\host\\libs", "\\\\server\\share")): + output = work / f"invalid-{index}.elf" + result = subprocess.run([str(relinker), "--rpath", run_path, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "run path must be absolute or begin with $ORIGIN" in result.stderr, ( + run_path, result.returncode, result.stdout, result.stderr) + assert not output.exists() assert not failures, failures print(f"Guest run path tests passed: {checked} module/path combinations") From 8c554cc42ad6b046d1652a18a46d8e99ff5af540 Mon Sep 17 00:00:00 2001 From: Cesar Rodriguez <11452991+Tamalero@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:57:17 -0700 Subject: [PATCH 236/353] fix(agc): accept a CB metadata pass whose depth test passes every fragment without writing DecodeColorMetadataPass refuses an eliminate-fast-clear or DCC-decompress pass when DB_DEPTH_CONTROL has any of its low four bits set and depth or stencil planes are bound. Marvel's Guardians of the Galaxy (PPSA01748) issues one with the Z test enabled, ZFUNC ALWAYS and no Z write (DB_DEPTH_CONTROL 0x72) while a Z32 and an S8 target are bound. That test passes every fragment and leaves depth unchanged, so the pass does no depth work. Draws already treat this state as a pass-through (depthPassThrough); the metadata pass now uses the same rule. Depth bounds, depth writes, stencil tests and DB_RENDER_CONTROL clears or copies are still refused, and the error names the four registers involved. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 13 ++++++++++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8a07f6bc87..306709333c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1190,7 +1190,18 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (mode != 2u && mode != 6u && !resolve) return std::nullopt; ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); - Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work"); + const auto value = [&](std::uint32_t offset) { + const auto found = find(cx, offset); + return found == cx.end() ? 0u : found->second; + }; + const bool planes = !depthPlanesAbsent(cx); + const auto depthControl = planes ? read(cx, 0x200) : 0u; + const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl)); + if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) { + char text[192]; + std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011)); + throw std::runtime_error(text); + } const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4589213ebe..e64c63adf0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1743,6 +1743,15 @@ void metadataPassTests() { queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); + auto bound = queue; + bound.context[0x010] = 0x3; + bound.context[0x011] = 0x1; + bound.context[0x200] = 0x72; + Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass"); + bound.context[0x200] = 0x76; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); + bound.context[0x200] = 0x7a; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); queue.context[0x91] = 0x40020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); queue.context[0x91] = 0x40040; From acbc573212cb01d406f0283e50551191231724c9 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:10:37 +0530 Subject: [PATCH 237/353] feat(libSceAgcDriver): skip CMASK decode on single-sample views with viewMip > 0 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 10 ++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8a07f6bc87..7ae52aca2a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1142,7 +1142,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } - if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) { + if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) { Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4589213ebe..1626858046 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1908,6 +1908,16 @@ void cmaskTests() { auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); + constexpr std::size_t blockBytes = 65536u; + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + auto higherMip = queue; + higherMip.context[0x318] = static_cast(mipBase >> 8u); + higherMip.context[0x390] = static_cast(mipBase >> 40u); + higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + higherMip.context[0x31b] = 1u << 26u; + const auto higherTarget = DecodeColorBuffer(higherMip.context, 0); + Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK"); auto unaddressed = queue; unaddressed.context[0x31f] = 0; unaddressed.context[0x398] = 0; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 47f53975e1..cf1926f52b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -74,7 +74,7 @@ - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page -- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware +- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. From 9849a1db64f11a896a6d40c333278a8ed3b06ec4 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 14:15:27 +0200 Subject: [PATCH 238/353] feat(libSceLibcInternal): export sys_siglist and sys_nsig PS5CEMU-HAR (PPSA99360, ps5-payload-dev build, relinks with --trap-sdk-syscall-stubs) imports the data objects sys_siglist (yCdGspbNHZ8) and sys_nsig (ZD+Dp+-LsGg) from libSceLibcInternal.prx; the import audit lists both as absent (registry entries 364 and 378, after pthread_setaffinity_np at 276). No run has reached them yet; the predicted stop line is FAIL: unresolved ELF import ZD+Dp+-LsGg (sys_nsig) from libSceLibcInternal.prx or yCdGspbNHZ8 (sys_siglist), whichever the loader resolves first. The table and count are FreeBSD 11.4 lib/libc/gen/siglist.c (8c639945): 32 descriptions, signal 0 to SIGUSR2 (NSIG 32), and sys_nsig is their number, 32. ps5-payload-dev declares both as objects exported by libSceLibcInternal and its strsignal reads them. Co-Authored-By: Claude Fable 5.1 --- .../prx/libSceLibcInternal/CMakeLists.txt | 12 +++++- .../prx/libSceLibcInternal/SignalList.cpp | 40 +++++++++++++++++++ core/libs/tests/GuestSignalList.cpp | 25 ++++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceLibcInternal/SignalList.cpp create mode 100644 core/libs/tests/GuestSignalList.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 17cbde0adf..b5dde2fe3f 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp Spawn.cpp UserContext.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp SignalList.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) @@ -115,4 +115,14 @@ if(BUILD_TESTING) set_tests_properties(guest_fminf PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_signal_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalList.cpp) + target_include_directories(guest_signal_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_list_tests PRIVATE libSceLibcInternal) + configure_windows_unwind(guest_signal_list_tests) + add_test(NAME guest_signal_list COMMAND guest_signal_list_tests) + set_tests_properties(guest_signal_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibcInternal/SignalList.cpp b/core/libs/prx/libSceLibcInternal/SignalList.cpp new file mode 100644 index 0000000000..c51fa70fe8 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/SignalList.cpp @@ -0,0 +1,40 @@ +extern "C" { + +extern const char* const sys_siglist_nid_postfix[32] = { + "Signal 0", + "Hangup", + "Interrupt", + "Quit", + "Illegal instruction", + "Trace/BPT trap", + "Abort trap", + "EMT trap", + "Floating point exception", + "Killed", + "Bus error", + "Segmentation fault", + "Bad system call", + "Broken pipe", + "Alarm clock", + "Terminated", + "Urgent I/O condition", + "Suspended (signal)", + "Suspended", + "Continued", + "Child exited", + "Stopped (tty input)", + "Stopped (tty output)", + "I/O possible", + "Cputime limit exceeded", + "Filesize limit exceeded", + "Virtual timer expired", + "Profiling timer expired", + "Window size changes", + "Information request", + "User defined signal 1", + "User defined signal 2", +}; + +extern const int sys_nsig_nid_postfix = sizeof(sys_siglist_nid_postfix) / sizeof(sys_siglist_nid_postfix[0]); + +} diff --git a/core/libs/tests/GuestSignalList.cpp b/core/libs/tests/GuestSignalList.cpp new file mode 100644 index 0000000000..8035660662 --- /dev/null +++ b/core/libs/tests/GuestSignalList.cpp @@ -0,0 +1,25 @@ +#include +#include + +extern "C" { +extern const char* const sys_siglist_nid_postfix[32]; +extern const int sys_nsig_nid_postfix; +} + +static constexpr int GUEST_NSIG = 32; +static constexpr int GUEST_SIGHUP = 1; +static constexpr int GUEST_SIGINT = 2; +static constexpr int GUEST_SIGSEGV = 11; +static constexpr int GUEST_SIGUSR2 = 31; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + Require(sys_nsig_nid_postfix == GUEST_NSIG); + Require(std::strcmp(sys_siglist_nid_postfix[0], "Signal 0") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGHUP], "Hangup") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGINT], "Interrupt") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGSEGV], "Segmentation fault") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGUSR2], "User defined signal 2") == 0); + for (int signal = 0; signal < sys_nsig_nid_postfix; ++signal) Require(sys_siglist_nid_postfix[signal] != nullptr && sys_siglist_nid_postfix[signal][0] != '\0'); +} diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 0a303516f1..02545609c1 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -12,5 +12,6 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) +- [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified From 8e0d6324af3fe9fee82b88e1c79efeb741068709 Mon Sep 17 00:00:00 2001 From: arcwiser Date: Mon, 12 Oct 2026 23:44:47 +1100 Subject: [PATCH 239/353] docs: group signal list debt with signal handling --- docs/dev/debt/libSceLibcInternal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 02545609c1..7cc67d0d95 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -12,6 +12,6 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read From fe5493849043879cda82a24df9fa188f1dc392de Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 12:27:25 +0100 Subject: [PATCH 240/353] feat(libkernel): report hw.realmem through sysctl The dolphin_libretro core of RetroArch (PPSA99169) reads {CTL_HW, HW_REALMEM} as the machine's memory size and stopped on the unsupported MIB. It reports the same value as hw.physmem. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/System/src/SystemConfiguration.cpp | 6 +++++- core/libs/tests/GuestSystemConfiguration.cpp | 13 +++++++------ docs/dev/debt/libkernel.md | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 796ee5c8ad..67838f043f 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -131,7 +131,7 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void if (model.empty()) throw std::runtime_error(std::string(__func__) + ": the host CPU has no CPUID brand string for hw.model"); return CopyOut(model.c_str(), model.size() + 1, oldValue, oldLength); } - if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (nameLength == 2 && name[0] == 6 && (name[1] == 5 || name[1] == 12)) { if (newValue != nullptr) { *__error_nid_postfix() = 1; return -1; @@ -234,6 +234,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 5}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } + if (std::strcmp(name, "hw.realmem") == 0) { + static constexpr int mib[] = {6, 12}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index b886f53e53..18dbb03b6b 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -66,8 +66,8 @@ static void CheckProcessorCountSysctl() { } Require(threw); } -static void CheckPhysicalMemorySysctl() { - const int mib[] = {6, 5}; +static void CheckPhysicalMemorySysctl(int number, const char* name) { + const int mib[] = {6, number}; const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; *__error_nid_postfix() = 13; std::size_t length = 0; @@ -77,15 +77,15 @@ static void CheckPhysicalMemorySysctl() { Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); value = 0; length = 16; - Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(sysctlbyname_nid_postfix(name, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); Require(*__error_nid_postfix() == 13); unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; length = 4; - Require(sysctlbyname_nid_postfix("hw.physmem", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(sysctlbyname_nid_postfix(name, bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); Require(length == 4 && bytes[4] == 0xaa); length = sizeof(value); Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); - Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix(name, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } #if defined(__x86_64__) || defined(__i386__) static void CheckModelSysctl() { @@ -267,7 +267,8 @@ static void CheckArandomSysctl() { } int main() { CheckProcessorCountSysctl(); - CheckPhysicalMemorySysctl(); + CheckPhysicalMemorySysctl(5, "hw.physmem"); + CheckPhysicalMemorySysctl(12, "hw.realmem"); #if defined(__x86_64__) || defined(__i386__) CheckModelSysctl(); #endif diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c1652dbe52..429bce44c4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -16,6 +16,7 @@ - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` +- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified From ccfadfbeef848238c65edc83294d3c31244c804b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 18:55:53 +0000 Subject: [PATCH 241/353] fix(libc): keep a trailing slash on paths under a mount alias Guest path resolution trimmed the trailing slash of a path under a mount alias (/savedata0, /download0, /app0 and the others), so the host saw "file" for "file/": unlink, remove, sceKernelUnlink and rename removed or moved the file, open opened it, and chmod and access succeeded on it. The same path outside an alias already failed with ENOTDIR, because the host got the slash. FreeBSD 11.4 rejects it in lookup() (sys/kern/vfs_lookup.c): /* * If we're processing a path with a trailing slash, * check that the end result is a directory. */ if ((cnp->cn_flags & TRAILINGSLASH) && dp->v_type != VDIR) { error = ENOTDIR; goto bad2; } On Linux and macOS a path under an alias now keeps its trailing slash on the host path, as paths outside an alias do, so the host fails with ENOTDIR for a file and directories with a trailing slash still work. Windows keeps the previous behaviour; TechnicalDebt records it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/General.cpp | 3 +++ core/libs/tests/GuestFilesystem.cpp | 13 +++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 17 insertions(+) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 133196f578..2f949184cf 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -107,6 +107,9 @@ std::optional ResolveAlias(const std::string& guestPath) if (!HasPrefix(relative, prefix)) continue; if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred(); std::filesystem::path result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); +#ifndef _WIN32 + if (guestPath.back() == '/') result /= ""; +#endif return result.make_preferred(); } return std::nullopt; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 6971f0f944..42d5f0e46a 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -170,6 +170,19 @@ static void TestParentTraversal() { checkInvalid("/parent-test-mount/present.txt/..", 20); FileStat status{}; Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); +#ifndef _WIN32 + for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; + Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); + Require(remove_nid_postfix("/parent-test-mount/slash-remove/") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/slash-rename/", "/parent-test-mount/slash-renamed") == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix("/parent-test-mount/slash-open/", 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelUnlink("/parent-test-mount/slash-kernel/") == static_cast(0x80020014u)); + for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) Require(std::filesystem::is_regular_file(root / name)); + Require(!std::filesystem::exists(root / "slash-renamed")); + Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000); + Require(mkdir_nid_postfix("/parent-test-mount/slash-directory/", 0755) == 0 && std::filesystem::is_directory(root / "slash-directory")); + Require(rmdir_nid_postfix("/parent-test-mount/slash-directory/") == 0 && !std::filesystem::exists(root / "slash-directory")); +#endif RemovePathAlias_nid_no_patch("parent-test-mount"); std::error_code linkError; std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 21227c9c1e..a28455c9c3 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -57,6 +57,7 @@ - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation. - [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. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) From f0c3a5c85e8e77e29b3bcbe569866649ab731a7d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 13 Oct 2026 19:33:02 +0000 Subject: [PATCH 242/353] fix(libc): find a relinked module whose name differs in case Borderlands 4 (PPSA01494) loads its UI library with sceKernelLoadStartModule("/app0/prx/libRenoirCore.PS5.prx"), but ships the module as prx/librenoircore.ps5.prx. On Linux the call fails: Could not load /app0/prx/libRenoirCore.PS5.prx result 0, handle result -2147352574 (0x80020002, ENOENT). Guest paths are resolved case-insensitively, but that only finds a component that exists. With the documented layout, app0/prx holds only the converted librenoircore.ps5.prx.guest.prx, so the requested name resolves to itself and RelinkedModulePath then looks for "libRenoirCore.PS5.prx.guest.prx" with its exact spelling. Windows and macOS hosts ignore the case, so only Linux is affected. RelinkedModulePath now falls back to the same ASCII case-insensitive match as ResolveHostPath when the exact .guest.prx is missing, and throws for an ambiguous match as that does. dlopen, sceKernelLoadStartModule, stat, open and sceKernelCheckReachability all go through it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/General.cpp | 11 ++++++++++- core/libs/tests/GuestFilesystem.cpp | 17 +++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 133196f578..eb31d784b5 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -384,7 +384,16 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file auto relinked = path; relinked += ".guest.prx"; std::error_code error; - return std::filesystem::is_regular_file(relinked, error) ? relinked : path; + if (std::filesystem::is_regular_file(relinked, error)) return relinked; + const auto name = relinked.filename().string(); + std::optional matched; + std::filesystem::directory_iterator entry(relinked.parent_path().empty() ? "." : relinked.parent_path(), error), end; + for (; !error && entry != end; entry.increment(error)) { + if (!SameName(entry->path().filename().string(), name)) continue; + if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); + matched = entry->path(); + } + return matched && std::filesystem::is_regular_file(*matched, error) ? *matched : path; } extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 6971f0f944..425c0c3c67 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -217,9 +217,26 @@ static void TestRelinkedModuleNames() { std::filesystem::remove_all(root); } +static void TestRelinkedModuleNameCase() { + const auto root = std::filesystem::path("anyps5-relinked-case-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + { std::ofstream file(root / "librenoircore.ps5.prx.guest.prx"); file << "relinked"; } + const auto requested = (root / "libRenoirCore.PS5.prx").generic_string(); + FileStat status{}; + Require(sceKernelStat(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(stat_nid_postfix(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(sceKernelCheckReachability(requested.c_str()) == 0); + const int opened = sceKernelOpen(requested.c_str(), 0, 0); + Require(opened >= 0 && sceKernelClose(opened) == 0); + Require(sceKernelStat((root / "libRenoirCore.PS5.prx2").generic_string().c_str(), &status) == static_cast(0x80020002u)); + std::filesystem::remove_all(root); +} + int main() { TestParentTraversal(); TestRelinkedModuleNames(); + TestRelinkedModuleNameCase(); Require(chmod_nid_postfix(nullptr, 0600) == -1 && *__error_nid_postfix() == 14); Require(sceKernelChmod_nid_postfix(nullptr, 0600) == static_cast(0x8002000eu)); Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 21227c9c1e..a6f710bc55 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -55,7 +55,7 @@ - [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. 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. Its 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. - [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. +- [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` - [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 - [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. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written From dad29cac432b9bc4d9ed588c020a736c8de74afe Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 14:47:20 +0100 Subject: [PATCH 243/353] feat(libkernel): report vm.overcommit through sysctlbyname The mimalloc in PS5MC (PPSA99420) reads vm.overcommit at start-up and stopped on the unsupported name. It reports FreeBSD's default, 0. Co-Authored-By: Claude Opus 5.5 --- .../System/src/SystemConfiguration.cpp | 5 +++++ core/libs/tests/GuestSystemConfiguration.cpp | 22 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 28 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c825755c14..c6a0c3ca44 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -206,6 +206,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } + if (std::strcmp(name, "vm.overcommit") == 0) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); + static constexpr int overcommit = 0; + return CopyOut(&overcommit, sizeof(overcommit), oldValue, oldLength); + } if (std::strcmp(name, "kern.sdk_version") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 8387046a50..2e362eec13 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -219,6 +219,27 @@ static void CheckPseudoRandomSysctl() { } Require(threw); } +static void CheckOvercommitSysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, &length, nullptr, 0) == 0 && length == sizeof(int)); + int value = -1; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("vm.overcommit", &value, &length, nullptr, 0) == 0 && value == 0 && length == sizeof(int)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("vm.overcommit", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + bool threw = false; + length = sizeof(value); + try { + sysctlbyname_nid_postfix("vm.overcommit", &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} static void CheckSdkVersionSysctl() { *__error_nid_postfix() = 13; std::size_t length = 0; @@ -282,6 +303,7 @@ int main() { CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); + CheckOvercommitSysctl(); CheckArandomSysctl(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(3) == 128); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f33e422567..d4b6aae8fd 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -17,6 +17,7 @@ - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` - [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified From e9249a0a47e15b9d42cd6c5c38379bbdb50c5294 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 12 Oct 2026 18:26:31 +0200 Subject: [PATCH 244/353] feat(agc): host import windows, least-recent eviction under the budget A registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses instead of whole, and the 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). An import past the budget retires the least recently used imports not used in the last 256 lookups. - An access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps, so uses across neighbouring edges share one import instead of replacing each other. - An address-based build (AcquireRegistered) imports the range whole; a window over its first byte does not serve it. - Every refusal (alignment, a failed run, a held import, the budget with what may be evicted) is decided before anything is retired. - An import made when a range is mapped never evicts: the map observer holds no GPU lock. The window size and the budget have setters for the tests. --- .../Graphics/include/GuestBufferMemory.hpp | 6 + .../Graphics/src/GuestBufferMemory.cpp | 246 ++++++++++++++---- docs/dev/ARCHITECTURE.md | 1 + docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 205 insertions(+), 49 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 2a74aebf18..e1b80ff503 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -35,6 +35,7 @@ struct HostImport { bool dmaBuf = false; bool joined = false; std::shared_ptr chunk; + mutable std::uint64_t lastUse = 0; }; enum class ImportWatch : std::uint8_t { Watch, Unwatch }; @@ -59,6 +60,10 @@ void SetImportWatch(const Context& context, ImportWatch watch); // the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); +// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). +void SetHostImportRangeWindow(std::uint64_t bytes); +void SetHostImportBudget(std::uint64_t bytes); +std::uint64_t HostImportLiveBytes(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Whether an existing import covers [address, address + bytes), without reconciling the imports // with the registry or making one (HostImportFor may take a registry lease): a hint for choices @@ -379,6 +384,7 @@ private: // Import registry epoch when `direct` pointers were taken at acquire time; they are reused while // no import was destroyed since. std::uint64_t importsEpoch = 0; + std::uint64_t importFloor = 0; std::vector regions; // Whether `regions` is in ascending address order (true right after AcquireRegistered, whose // regions follow the registry's order), so AddSnapshot can search instead of scanning. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 5813a30417..23756e0f47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -172,6 +172,8 @@ struct HostImports { VkDevice watchDevice = VK_NULL_HANDLE; bool unwatchImports = false; bool unwatchDmaBufImports = false; + std::uint64_t useCounter = 0; + std::uint64_t buildFloor = 0; }; HostImports& Imports() { @@ -582,6 +584,13 @@ std::shared_ptr leasedRangeOwner(const GuestAlloc return found != lease.end() && (*found)->address == base ? *found : nullptr; } +std::shared_ptr leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; }); + if (found == lease.begin()) return nullptr; + const auto& range = *std::prev(found); + return begin >= range->address && end <= range->address + range->bytes ? range : nullptr; +} + struct ImportRun { std::uint64_t base = 0; std::uint64_t bytes = 0; @@ -683,22 +692,131 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { } bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { + if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) { + const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes); + return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); + } return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base)); } +bool pointerHeld(const HostImports& state, const HostImport& entry) { + return entry.lastUse >= state.buildFloor; +} + +bool evictable(const HostImports& state, const HostImport& entry) { + constexpr std::uint64_t protectedUses = 256; + return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter; +} + +std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) { + std::uint64_t freed = 0; + while (freed < wanted) { + auto oldest = state.imports.end(); + for (auto it = state.imports.begin(); it != state.imports.end(); ++it) { + if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it; + } + if (oldest == state.imports.end()) break; + freed += oldest->second.bytes; + retireImport(context.device, state, oldest, lease); + } + return freed; +} + +std::uint64_t physicalMemoryBytes() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0; +#else + const auto pages = sysconf(_SC_PHYS_PAGES); + const auto pageSize = sysconf(_SC_PAGESIZE); + return pages > 0 && pageSize > 0 ? static_cast(pages) * static_cast(pageSize) : 0; +#endif +} + +std::atomic budgetOverride{0}; + +// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it +// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical +// memory, at most the largest host heap less 2 GiB. +std::uint64_t importBudget(const Context& context) { + if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set; + static const std::uint64_t configured = []() -> std::uint64_t { + const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); + return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u; + }(); + if (configured != 0) return configured; + std::uint64_t hostHeap = 0; + for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) { + if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max(hostHeap, context.memory.memoryHeaps[index].size); + } + const auto fromMemory = std::max(6144ull << 20u, physicalMemoryBytes() / 2); + constexpr std::uint64_t reserve = 2048ull << 20u; + return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory; +} + +std::atomic rangeImportWindow{64ull << 20u}; + +// The import of an access to a registered range larger than the range window: the cells the access touches, grown to +// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of +// replacing each other's. +ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) { + const auto window = rangeImportWindow.load(std::memory_order_relaxed); + if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run; + const auto& range = *run.ranges.front(); + if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run; + const auto lastCell = range.bytes / window - 1; + const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); }; + auto windowBegin = range.address + cell(begin) * window; + const auto endCell = cell(end - 1); + auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window; + for (bool grown = true; grown;) { + grown = false; + auto it = state.imports.upper_bound(windowBegin); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < windowEnd; ++it) { + const auto importEnd = it->second.base + it->second.bytes; + if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue; + if (it->second.base < windowBegin || importEnd > windowEnd) grown = true; + windowBegin = std::min(windowBegin, it->second.base); + windowEnd = std::max(windowEnd, importEnd); + } + } + if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run; + run.base = windowBegin; + run.bytes = windowEnd - windowBegin; + return run; +} + 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); +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); -const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) { - if (!importsStale(context, state)) { - if (const auto* current = findImport(state, address, address + 1)) return current; - } else if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { - return current; +// The bytes retireLeastRecent could free outside [begin, end). +std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) { + std::uint64_t bytes = 0; + for (const auto& [base, entry] : state.imports) { + if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes; + } + return bytes; +} + +const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { + const bool fresh = !importsStale(context, state); + 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; } auto run = importRun(lease, address); + // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the + // first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below. + if (fresh && end == 0) { + if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current; + } + if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment); if (run.ranges.empty()) return nullptr; - return importRunAt(context, state, std::move(run), false, address, lease); + return importRunAt(context, state, std::move(run), false, address, lease, end, evict); } const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { @@ -707,10 +825,10 @@ const HostImport* importJoined(const Context& context, HostImports& state, std:: auto run = joinedRun(lease, begin, end); if (run.ranges.empty()) return nullptr; if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr; - return importRunAt(context, state, std::move(run), true, begin, lease); + return importRunAt(context, state, std::move(run), true, begin, lease, end, true); } -const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease) { +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) { const auto base = run.base; const auto bytes = run.bytes; auto& failures = joined ? state.failedJoins : state.failed; @@ -719,30 +837,24 @@ 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()) { - if (sameRun(found->second, run)) return &found->second; - retireImport(context.device, state, found, lease); - } - for (auto it = state.imports.lower_bound(base); it != state.imports.end() && it->first < base + bytes;) { - const auto next = std::next(it); - retireImport(context.device, state, it, lease); - it = next; - } - if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease); + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) 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; failures.erase(failed); } - // Pinned imports count against the driver's system memory budget; past it ordinary host - // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the - // registered memory of address-based shaders; past it they copy gigabytes per dispatch). - static const std::uint64_t budget = [] { - const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); - return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u; - }(); - const auto live = state.liveBytes; - if (live + bytes > budget) { + // The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with + // what may be evicted) before any of them is retired. + auto first = state.imports.lower_bound(base); + if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; + std::uint64_t replaced = 0; + for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { + if (pointerHeld(state, it->second)) return nullptr; + replaced += it->second.bytes; + } + const auto budget = importBudget(context); + const auto live = state.liveBytes - replaced; + if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) { // A refused import turns every later use of the range into CPU copies, so say so. static std::uint64_t refused = 0, refusedBytes = 0; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); @@ -750,10 +862,16 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import refusedBytes += bytes; if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { lastReport = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(live >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); + std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); } return nullptr; } + for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { + const auto next = std::next(it); + retireImport(context.device, state, it, lease); + it = next; + } + if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; entry.range = run.ranges.front(); entry.ranges = std::move(run.ranges); @@ -795,7 +913,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return nullptr; } state.unaliasedSpans.insert(base); - return importAllocation(context, state, address, lease); + return importAllocation(context, state, address, lease, end, evict); } } #else @@ -863,15 +981,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import state.liveBytes += bytes; if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); entry.joined = joined; + entry.lastUse = ++state.useCounter; return &state.imports.emplace(base, entry).first->second; } -// The lease is in address order (it is built from the registry map), so a range is found by binary search. -const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) { - const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; }); - return found != lease.end() && (*found)->address == base ? found->get() : nullptr; -} - // The readable registered range containing [begin, end), or null. const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; }); @@ -909,14 +1022,13 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc if (generation == state.refreshedGeneration) return; state.refreshedGeneration = generation; for (auto it = state.imports.begin(); it != state.imports.end();) { - const auto* range = leasedRangeAt(lease, it->first); - if (range != nullptr && sameRange(it->second, lease)) { + if (sameRange(it->second, lease)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); ++it; continue; } static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; - if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); + if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); const auto next = std::next(it); retireImport(context.device, state, it, lease); it = next; @@ -930,7 +1042,15 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6 if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr; + if (begin < entry.base || end > entry.base + entry.bytes) return 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) { + if (!runCovers(lease, begin, end)) return findImport(state, begin, end); + const auto* entry = importAllocation(context, state, begin, lease, end); + return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr; } // Whether the imports need reconciling before a lookup: the registry changed since the last walk. @@ -2063,6 +2183,20 @@ void SetHostImportWindow(std::uint64_t bytes) { importWindow.store(bytes, std::memory_order_relaxed); } +void SetHostImportRangeWindow(std::uint64_t bytes) { + rangeImportWindow.store(bytes, std::memory_order_relaxed); +} + +void SetHostImportBudget(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); +} + +std::uint64_t HostImportLiveBytes() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return state.liveBytes; +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); @@ -2070,13 +2204,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s std::lock_guard lock(state.mutex); // A hit is only valid while the registry has not changed since the imports were reconciled. if (!importsStale(context, state)) { - if (const auto* entry = findImport(state, address, address + bytes)) return entry; + if (const auto* entry = findImport(state, address, address + bytes)) { + entry->lastUse = ++state.useCounter; + return entry; + } } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - if (!runCovers(lease, address, address + bytes)) return findImport(state, address, address + bytes); - const auto* entry = importAllocation(context, state, address, lease); - return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr; + return importAccess(context, state, address, address + bytes, lease); } bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { @@ -2099,7 +2234,7 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false; - if (importAllocation(context, state, range->address, current) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); + if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast(range->address), range->bytes, error.what()); } @@ -2199,6 +2334,13 @@ void GuestBufferMemory::AcquireRegistered() { else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed); else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed); else hit = true; + importFloor = state.useCounter + 1; + if (hit) { + const auto stamp = ++state.useCounter; + for (const auto& region : current->base) { + if (region.direct != nullptr) region.direct->lastUse = stamp; + } + } } if (hit) { for (auto* mirror : current->sparseMirrors) { @@ -2237,6 +2379,8 @@ void GuestBufferMemory::AcquireRegistered() { auto& state = Imports(); std::lock_guard lock(state.mutex); if (importable) refreshImports(context, state, lease); + if (importFloor == 0) importFloor = state.useCounter + 1; + state.buildFloor = importFloor; // Taken before the pass: the imports it takes are the registry's ones (just reconciled, so // none is retired by a make below), and a mirror's preparation may sync the recorder, whose // completions can retire an import already taken; that moves the epoch, and Upload then @@ -2258,7 +2402,7 @@ void GuestBufferMemory::AcquireRegistered() { if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); - entry = importAllocation(context, state, range->address, lease); + entry = importAllocation(context, state, range->address, lease, 0, false); } lap(timing.importsUs, at); if (entry != nullptr) { @@ -2907,6 +3051,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); + if (importFloor == 0) importFloor = state.useCounter + 1; const bool stale = importsStale(context, state); for (auto& region : regions) { if (region.mirror != nullptr) continue; @@ -2989,6 +3134,11 @@ void GuestBufferMemory::UploadFinish(bool addressable) { } uploaded = true; for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); + if (context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1; + } // Registered ranges to import from. Address-based shaders hold their lease until write-back; // descriptor-only uploads acquire one only when the registry changed or an import must be made // (a lease copies every registered range's shared_ptr under the registry lock), so the guest can @@ -3086,9 +3236,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { region.direct = nullptr; if (entry == nullptr) entry = findImport(state, region.begin, region.end); if (entry == nullptr) { - if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges()); - if (entry != nullptr && region.end > entry->base + entry->bytes) entry = importJoined(context, state, region.begin, region.end, importRanges()); - if (entry != nullptr && (region.begin < entry->base || region.end > entry->base + entry->bytes)) entry = nullptr; + entry = importAccess(context, state, region.begin, region.end, importRanges()); } // A staged region (see stagingEligible) is copied out of the import even when aligned; // without a recorder to record the copies it binds in place like any other. diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 5227da96e6..4eaebccf35 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -105,6 +105,7 @@ flowchart LR - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). +- Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). ### Untracked storage image comparisons diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cf1926f52b..d14eb624f2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,3 +114,4 @@ - [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. - [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. From c871b5893de7b665a55b099dd5da307734593eb6 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Mon, 12 Oct 2026 18:25:13 +0200 Subject: [PATCH 245/353] test(agc): host import windows, whole-range builds and budget refusals agc_driver_graphics (BdaResources.cpp), each failing without its change: - a view in the last window of a range whose first window was imported binds through an import of the whole range; - an address-based build over the budget leaves the range's window in place (3 builds, same import); - two uses across neighbouring cell edges share one import over 10 rounds; - a range mapped at the budget is refused at map time and retires nothing. agc_driver_host_import_windows: windows follow their range, and ClearHostImports drops them. --- .../libSceAgcDriver/tests/BdaResources.cpp | 153 ++++++++++++++++++ .../tests/execution/HostImportWindows.cpp | 145 +++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 85d5605604..08fe07ffa1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -325,6 +325,155 @@ void importJoinTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the +// test and reset after it. +class WindowedRange { +public: + WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) { + block = ::operator new(bytes, std::align_val_t{piece}); + std::memset(block, 0x33, bytes); + importing.hostImportAlignment = piece; + SetHostImportRangeWindow(window); + SetHostImportBudget(budget); + change(true); + } + ~WindowedRange() { + change(false); + SetHostImportBudget(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ::operator delete(block, std::align_val_t{piece}); + } + WindowedRange(const WindowedRange&) = delete; + WindowedRange& operator=(const WindowedRange&) = delete; + + std::uintptr_t First() const { return reinterpret_cast(block); } + + static constexpr std::size_t piece = 65536; + const std::size_t window; + const std::size_t bytes; + Context importing; + +private: + void change(bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + + void* block = nullptr; +}; + +// A registered range larger than the import window, its first window imported by an earlier access: an address-based +// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw +// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows). +void importWholeRangeOverWindowTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 2); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window"); + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment); + Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range"); + leased.WriteBack(); +} + +// 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) { + WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported"); + const auto buffer = windowed->buffer; + for (int build = 0; build < 3; ++build) { + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")"); + } + const auto* kept = HostImportFor(range.importing, first + 16, 32); + Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import"); +} + +// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows +// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at +// every use. +void importNeighbouringEdgesTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 3); + const auto first = range.First(); + const auto use = [&](std::uintptr_t address) { + GuestBufferMemory leased(range.importing); + leased.AddReadable(address, 32); + leased.Upload(false); + leased.WriteBack(); + }; + const auto acrossFirst = first + range.window - 16; + const auto acrossSecond = first + 2 * range.window - 16; + use(acrossFirst); + Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported"); + use(acrossSecond); + Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union"); + const auto serial = HostImportSerial(range.importing, first, range.bytes, false); + for (int round = 0; round < 10; ++round) { + use(acrossFirst); + use(acrossSecond); + Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")"); + } +} + +// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map +// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). +void importAtMapOverBudgetTests(const Context& context) { + constexpr std::size_t bytes = 65536; + auto importing = context; + importing.hostImportAlignment = bytes; + void* kept = ::operator new(bytes, std::align_val_t{bytes}); + void* used = ::operator new(bytes, std::align_val_t{bytes}); + void* mapped = ::operator new(bytes, std::align_val_t{bytes}); + const auto change = [&](void* block, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(kept, true); + change(used, true); + const auto keptAddress = reinterpret_cast(kept); + const auto usedAddress = reinterpret_cast(used); + Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made"); + for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made"); + { + GuestBufferMemory leased(importing); + leased.AddReadable(usedAddress, 16); + leased.Upload(false); + leased.WriteBack(); + } + const auto live = HostImportLiveBytes(); + SetHostImportBudget(live + bytes - 1); + change(mapped, true); + GuestAllocations::Mapped ranges; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == reinterpret_cast(mapped)) ranges.push_back(range); + } + } + Require(ranges.size() == 1, "the mapped range is not registered"); + static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + SetHostImportBudget(0); + Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)"); + Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired"); + for (auto* block : {kept, used, mapped}) change(block, false); + for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1323,6 +1472,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceImportRunTests(context); importWindowTests(context); importJoinTests(context); + importWholeRangeOverWindowTests(context); + importRefusedWholeRangeTests(context); + importNeighbouringEdgesTests(context); + importAtMapOverBudgetTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp new file mode 100644 index 0000000000..57396daef2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Graphics = AgcDriver::Graphics; +using Graphics::Require; + +constexpr std::uint64_t MiB = 1ull << 20u; +constexpr std::uint64_t WindowBytes = 64 * MiB; +constexpr std::uint64_t BlockBytes = 2 * WindowBytes; +constexpr std::uint64_t OtherBytes = 64 * 1024; +constexpr std::uint64_t GuestAlignment = 64 * 1024; +constexpr std::uint64_t ProbeBytes = 4096; + +std::string Hex(std::uint64_t value) { + char text[32]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::uint64_t size) : bytes(size) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestAlignment, bytes)); +#endif + Require(block != nullptr, "host import windows: cannot allocate a guest block of " + Hex(bytes)); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, 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() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; + std::uint64_t bytes = 0; +}; + +enum class Outcome : std::uint8_t { NoDevice, NoImport, Passed }; + +Graphics::Context Probe(const AgcDriver::VulkanDevice& device) { + Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return probe; +} + +bool Fill(AgcDriver::VulkanDevice& device, std::uint64_t address) { + const std::array pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(address, ProbeBytes, pattern); + device.WaitIdle(); + return filled; +} + +Outcome ClearingDropsImports() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + if (!Fill(*device, guest.Address() + WindowBytes)) return Outcome::NoImport; + const auto probe = Probe(*device); + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Graphics::ClearHostImports(device->Device()); + Require(!Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: ClearHostImports kept the window of " + Hex(guest.Address())); + return Outcome::Passed; +} + +Outcome WindowsFollowTheirRange() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + const auto probe = Probe(*device); + const auto address = guest.Address() + WindowBytes + WindowBytes / 2; + if (!Fill(*device, address)) return Outcome::NoImport; + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Require(!Graphics::HostImportCovers(probe, guest.Address(), ProbeBytes), "host import windows: the first window of " + Hex(guest.Address()) + " is imported along with the second"); + const auto first = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(first != 0, "host import windows: the window at " + Hex(address) + " has no serial"); + + GuestBlock other(OtherBytes); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after an unrelated registration"); + Require(Graphics::HostImportSerial(probe, address, ProbeBytes, false) == first, "host import windows: the window at " + Hex(address) + " was dropped by an unrelated registration, although its range still owns it"); + + GuestAllocations::Mutation().Remove(guest.Data()); + GuestAllocations::Mutation().Add(guest.Data(), BlockBytes, true, true, true); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after its range was mapped again"); + const auto second = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(second != 0 && second != first, "host import windows: the window of the range mapped again kept the serial " + Hex(first)); + return Outcome::Passed; +} + +} + +int main() { + try { + const auto counting = ClearingDropsImports(); + if (counting == Outcome::NoDevice) return VulkanTestSkipped; + if (counting == Outcome::NoImport) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + const auto windows = WindowsFollowTheirRange(); + Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); + std::puts("host import windows tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From c29e06c6938d51ef4263778f869c4c97901c1024 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:51:14 +0200 Subject: [PATCH 246/353] fix(agc): a joined host import may replace imports the build in flight already holds A join runs before any region confirms an import and a retired import lives through the retire-hold, so refusing it for held overlaps kept the joined run copied for the rest of the build. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 23756e0f47..8f0be0a68e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -848,8 +848,12 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; + // A join may replace imports this build already looked up: it runs before any region confirms an import + // (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the + // hold (KeepThroughHold). Refusing it there would keep the run copied for good. + const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); }; for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { - if (pointerHeld(state, it->second)) return nullptr; + if (held(it->second)) return nullptr; replaced += it->second.bytes; } const auto budget = importBudget(context); From fbf619ef0d05fe28a16e2407e6f39de7a952b6fd Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:28:23 +0530 Subject: [PATCH 247/353] feat(libSceAgc): ignore address argument on non-sample-dump events in sceAgcDcbEventWrite --- core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp | 1 - core/libs/prx/libSceAgc/tests/Commands.cpp | 44 ++++++++++++++++++++ docs/dev/debt/libSceAgc.md | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index e7271811c2..96c45f0d3c 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev Agc::Command::CheckAddress(guestAddress, 8, __func__); return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast(guestAddress), static_cast(guestAddress >> 32u)}, __func__); } - Agc::Command::Require(address == nullptr, __func__, "this event does not use an address"); const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u; return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 03aeff9061..6fc0d679e7 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -10,6 +10,8 @@ #include #include +extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address); +extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType); extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); @@ -641,6 +643,47 @@ void testDefaults() { expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } +void testEventWrite() { + Storage storage; + const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1000)); + check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer"); + check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header"); + check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word"); + check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word"); + check(storage.words[3] == 0u, "incorrect event 0x38 high address word"); + check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size"); + check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size"); + + Storage sample39; + sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast(0x2008)); + check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet"); + + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1001)); }); + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); }); + + Storage partialFlush; + sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr); + check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet"); + check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size"); + + Storage partialFlush15; + sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr); + check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet"); + + Storage partialFlush16; + sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr); + check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet"); + + Storage cacheFlushNull; + sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr); + check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet"); + check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size"); + + Storage cacheFlushWithAddress; + sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast(0x2d2a10000ull)); + check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet"); +} + } int main(int argc, char** argv) { @@ -652,6 +695,7 @@ int main(int argc, char** argv) { return 0; } testPackets(); + testEventWrite(); testNop(); testClearState(); testIndexedIndirectDraws(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index cf540b7c52..2a41360807 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -38,3 +38,4 @@ - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 5acd68f9494532ec07a7c9d26b6961842218f325 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Tue, 13 Oct 2026 22:13:19 +0530 Subject: [PATCH 248/353] perf(agc): do not store a compute fill a multisampled target takes as its clear A multisampled color target keeps its samples only in its host image, and every guest read of its color memory that the driver checks is refused (RequireNoMultisampleTarget). A uniform compute fill over exactly its color memory with an exact clear value becomes the target's clear through NoteColorMetadataFill, so BufferFill no longer stores it into the host import as well. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Memory/BufferFill.cpp | 21 +- .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 9 + .../execution/MultisampleFillHostOnly.cpp | 251 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp 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 5a28dc83fb..62c6153a13 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -252,7 +252,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span fillPhaseMs{}; static std::array holdByCoverMs{}; static std::array holdByCover{}; - static std::uint64_t flushes = 0, flushed = 0; + static std::uint64_t flushes = 0, flushed = 0, absorbedFills = 0, absorbedBytes = 0; static auto fillReport = std::chrono::steady_clock::now(); ++fills; filledBytes += bytes; @@ -302,19 +302,23 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanFillBuffer(base, bytes, pattern); + const bool stored = cleared || absorbed || localDevice->FillBuffer(base, bytes, pattern); phase(FillDevice); - if (stored && !cleared) { + if (stored && !cleared && !absorbed) { if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled); } - if (profile && stored && !cleared) { - const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; + if (profile && stored && !cleared && !absorbed) { ++(uniform ? uniformFills : patternFills); (uniform ? uniformBytes : patternBytes) += bytes; if (!uniform) chainCopies += static_cast(std::bit_width((bytes / 16) - 1)); @@ -331,7 +335,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(block).first(std::min(chunk, bytes - done)), 16); phase(FillCpu); } - if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) { + if (uniform) { Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); } @@ -349,6 +353,9 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(absorbedFills), static_cast(absorbedBytes) / 1048576.0); + line += absorbedText; line += "; hold by cover (count/ms):"; for (std::size_t i = 0; i < coverNames.size(); ++i) { std::snprintf(text, sizeof(text), " %s %llu/%.0f", coverNames[i], static_cast(holdByCover[i]), holdByCoverMs[i]); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 50674497b0..bfcc8fd534 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -50,6 +50,7 @@ bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, V bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); +bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void ClearMultisampleTargets(VkDevice device); void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 1ee9499274..792b1395a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -402,6 +402,15 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, destination.MarkDirty(); } +bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + if (bytes == 0) return false; + std::lock_guard lock(targetsMutex()); + return std::any_of(targets().begin(), targets().end(), [&](const auto& target) { + VkClearColorValue clear{}; + return target->active->load(std::memory_order_acquire) && target->target.address == address && MultisampledColorBytes(target->target) == bytes && MultisampledFillClearColor(target->target, pattern, clear); + }); +} + void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); for (const auto& target : targets()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp new file mode 100644 index 0000000000..1f4a518a9c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp @@ -0,0 +1,251 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; +constexpr std::byte Marker{0xab}; + +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, "multisampled fill: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::byte* Data() { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + + bool Holds(std::byte value, std::size_t offset, std::size_t bytes) const { + return std::all_of(block + offset, block + offset + bytes, [&](std::byte held) { return held == value; }); + } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Submit(std::initializer_list> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "multisampled fill: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, std::uint64_t base, std::uint32_t bytes, std::uint32_t value) { + const auto kernel = code.Address(); + Submit({ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 64}), + Pm4Packet(0x76, {0x208, 1}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (8u << 1u) | 0x80u}), + Pm4Packet(0x76, {0x240, Low(base)}), + Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), + Pm4Packet(0x76, {0x242, bytes / 16u}), + Pm4Packet(0x76, {0x243, 0x4bfacu}), + Pm4Packet(0x76, {0x244, value}), + Pm4Packet(0x76, {0x245, value}), + Pm4Packet(0x76, {0x246, value}), + Pm4Packet(0x76, {0x247, value}), + Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), + }); +} + +void Settle(const GuestBlock& code, const GuestBlock& sentinel) { + static std::uint32_t round = 0; + ++round; + DispatchFill(code, sentinel.Address(), 1024, round); + std::array stored{}; + AgcDriver::GuestMemory::Read(sentinel.Address(), std::as_writable_bytes(std::span(stored)), 4); + Require(stored == std::array{round, round, round, round}, "the sentinel fill did not land"); +} + +ColorTarget MultisampledTarget(const GuestBlock& color) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + AgcDriver::Graphics::ClearCachedTextures(handle); + } + } clearTargets{device, context.device}; + auto& code = NewBlock(); + std::memcpy(code.Data(), FillKernel.data(), sizeof(FillKernel)); + + auto& sentinel = NewBlock(); + auto& control = NewBlock(); + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + if (control.Holds(Marker, 0, BlockBytes)) { + std::cout << "skipped, the device does not store compute fills into guest memory\n"; + return false; + } + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of 0 over plain guest memory did not store 0"); + + auto& color = NewBlock(); + const auto target = MultisampledTarget(color); + Require(AgcDriver::Graphics::MultisampledColorBytes(target) == BlockBytes, "the 4x 64x64 RGBA8 target does not take one 64 KiB block"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill of 0 over the whole color memory is not taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes / 2, 0), "a fill over half of the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address() + 4096, BlockBytes - 4096, 0), "a fill starting inside the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), 2 * BlockBytes, 0), "a fill reaching past the color memory is taken as the target's clear"); + + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(Marker, 0, BlockBytes), "a compute fill taken as the multisampled target's clear was stored into its color memory"); + DispatchFill(code, color.Address(), BlockBytes / 2, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes / 2) && color.Holds(Marker, BlockBytes / 2, BlockBytes / 2), "a compute fill over half of the color memory was not stored"); + + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of plain guest memory was not stored once a multisampled target exists"); + auto& srgbColor = NewBlock(); + auto srgb = MultisampledTarget(srgbColor); + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::MultisampleTargetView(context, srgb) != VK_NULL_HANDLE, "the sRGB multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0), "a fill of 0 over an sRGB target's color memory is not taken as its clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0x80808080u), "a fill with no exact sRGB clear value is taken as the target's clear"); + std::memset(srgbColor.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, srgbColor.Address(), BlockBytes, 0x80808080u); + Settle(code, sentinel); + Require(srgbColor.Holds(std::byte{0x80}, 0, BlockBytes), "a compute fill with no exact clear value was not stored into the color memory"); + + { + GuestAllocations::Mutation mutation; + mutation.Remove(color.Data()); + mutation.Add(color.Data(), BlockBytes, true, true, true); + } + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill over a retired target's color memory is taken as its clear"); + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes), "a compute fill over a retired target's color memory was not stored"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = FillsTakenAsMultisampledClearsAreNotStored(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled fill tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + LibcRunShutdown_nid_postfix(); + } catch (...) { + } + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cf1926f52b..0bb804f343 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,7 +80,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. -- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. +- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. From e092d2a3fc3ada8386d33e23d094cf5872f9b2ae Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:37:49 -0700 Subject: [PATCH 249/353] test(shader): check uniform table images after the descriptor phis #757 and #758 add their image table checks where this check was, after verifyBindlessTable, so the pull requests conflicted. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4d7fbbd0a5..ed7b58a1b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -947,63 +947,6 @@ void verifyBindlessTable() { fillSrt(4u); } -void verifyUniformTableImage() { - using namespace ShaderRecompiler; - constexpr std::uint32_t Format8888UNorm = 56; - constexpr std::uint32_t Type2D = 9; - struct alignas(256) Texture { std::array bytes{}; }; - static Texture textures[2]; - struct alignas(4096) GuestTables { - std::array, 3> table{}; - std::array output{}; - std::array srt{}; - }; - static GuestTables guest; - const auto texture = [](const Texture& image) { - const auto base = static_cast(reinterpret_cast(image.bytes.data())); - return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; - }; - const auto first = texture(textures[0]); - const auto second = texture(textures[1]); - std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); - std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); - const auto bufferDescriptor = [](const void* base, std::uint32_t records) { - const auto address = static_cast(reinterpret_cast(base)); - return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; - }; - const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); - const auto outputV = bufferDescriptor(guest.output.data(), 16u); - std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); - std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); - const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); - std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; - const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; - RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; - request.context.waveSize = 64; - request.context.userDataBaseRegister = 0; - request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; - request.target.vulkanVersion = 0x00401000u; - request.target.spirvVersion = 0x00010300u; - request.target.subgroupSize = 32; - request.target.supportedCapabilities = capabilities; - request.target.supportedExtensions = extensions; - request.target.bdaAbiVersion = BdaAbi::Version; - request.target.fragmentShaderBarycentricEnabled = false; - request.layout.pushConstantSizeBytes = 128; - const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); - for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { - userData[2] = key; - AgcDriver::ShaderMemory memory({}); - const auto capture = memory.Capture(request); - require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); - } -} - void verifyDescriptorPhis() { using namespace ShaderRecompiler; @@ -1177,6 +1120,63 @@ void verifyDescriptorPhis() { expectFailure([&] { static_cast(planImages(splitReloads(260u))); }, "GetImageResource dword 0 is not a valid runtime value", "descriptor Phi: a web over more than 512 descriptor tuples was accepted"); } +void verifyUniformTableImage() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + struct alignas(4096) GuestTables { + std::array, 3> table{}; + std::array output{}; + std::array srt{}; + }; + static GuestTables guest; + const auto texture = [](const Texture& image) { + const auto base = static_cast(reinterpret_cast(image.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto first = texture(textures[0]); + const auto second = texture(textures[1]); + std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); + std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); + const auto bufferDescriptor = [](const void* base, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; + }; + const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); + const auto outputV = bufferDescriptor(guest.output.data(), 16u); + std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); + std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); + for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { + userData[2] = key; + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + } +} + void verifyLaneSelectedDescriptors() { std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf40c0800u, 0xfa000060u, 0xbf8cc07fu, 0x7d880090u, 0xbf068010u, 0xbf850011u}; for (std::uint32_t dword = 0; dword < 8u; dword++) { @@ -3614,8 +3614,8 @@ int main(int argc, char** argv) { verifyPureFlatSlots(); verifyGuardedDescriptorSources(); verifyBindlessTable(); - verifyUniformTableImage(); verifyDescriptorPhis(); + verifyUniformTableImage(); verifyLaneSelectedDescriptors(); verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); From 875ecc139114660b43760050bcaa7c871e6a0b31 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:35:39 -0400 Subject: [PATCH 250/353] fix(agc): count depth plane uploads toward the recorder's kept bytes A depth plane copy reads every guest layer that no depth surface holds into a host-visible upload buffer, which the recording batch keeps until it completes. The batch kept the uploads without their size, so Recorder::BoundKeptBytes neither submitted the open batch nor waited for older batches while uploads piled up: with the GPU behind, thousands of them stayed in flight and host-visible allocations failed with VK_ERROR_OUT_OF_DEVICE_MEMORY once the host heap's budget was used up. Keep each upload with its size, as texture uploads are kept. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/DepthSurface.cpp | 2 +- .../tests/execution/DepthPlaneGuestLayers.cpp | 46 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index d3379ee102..f3a68896ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -400,7 +400,7 @@ private: regions.push_back(region(layer, aspect(), layer)); } if (recorder != nullptr) { - for (const auto& upload : uploads) recorder->Keep(upload); + for (const auto& upload : uploads) recorder->Keep(upload, upload->Bytes().size()); } } VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index a04e690b2c..797f086287 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -225,15 +226,7 @@ void FillLayer(std::uint8_t* layer, float depth) { for (std::size_t offset = 0; offset < LayerBytes; offset += sizeof(depth)) std::memcpy(layer + offset, &depth, sizeof(depth)); } -void Run(const Context& base, const WatchedBlock& block) { - TextureDetiler detiler(base); - auto context = base; - context.detiler = &detiler; - const auto address = block.Address(); - FillLayer(block.Data(), 1.0f); - FillLayer(block.Data() + LayerBytes, 0.25f); - Require(DepthSliceBytes({Side, Side}, 4u) == LayerBytes, "the depth slice of the test surface is not one layer of the guest block"); - DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); +GuestTextureResource ArrayOver(std::uint64_t address) { GuestTextureResource resource{}; resource.baseAddress = address; resource.width = Side; @@ -247,8 +240,19 @@ void Run(const Context& base, const WatchedBlock& block) { resource.dstSelY = 5; resource.dstSelZ = 6; resource.dstSelW = 7; - const std::array words{}; - const VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + return resource; +} + +constexpr std::array words{}; +constexpr VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + +void Run(const Context& context, const WatchedBlock& block) { + const auto address = block.Address(); + FillLayer(block.Data(), 1.0f); + FillLayer(block.Data() + LayerBytes, 0.25f); + Require(DepthSliceBytes({Side, Side}, 4u) == LayerBytes, "the depth slice of the test surface is not one layer of the guest block"); + DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto resource = ArrayOver(address); watchedBegin = address + LayerBytes; watchedEnd = address + BlockBytes; AgcDriver::GuestMemory::SetFlushHook(&CountReads); @@ -295,6 +299,20 @@ void Run(const Context& base, const WatchedBlock& block) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void RecordedUploads(const Context& context, const WatchedBlock& block) { + const auto resource = ArrayOver(block.Address()); + Recorder recorder(context); + recorder.Activate(); + for (int round = 1; round <= 3; ++round) { + FillLayer(block.Data() + LayerBytes, 0.125f * static_cast(round)); + const auto before = recorder.OpenKeptBytes(); + Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "the 2D array over the depth surface was not sampled through its depth planes while recording"); + const auto kept = recorder.OpenKeptBytes() - before; + Require(kept >= LayerBytes, "the recorded upload of a written guest layer added " + std::to_string(kept) + " bytes to the open batch's kept bytes, less than the " + std::to_string(LayerBytes) + " bytes of the layer it holds until the batch completes"); + } + recorder.Sync(); +} + } int main() { @@ -312,9 +330,13 @@ int main() { return Skipped; } { + TextureDetiler detiler(device->GetContext()); + auto context = device->GetContext(); + context.detiler = &detiler; WatchedBlock block; Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); - Run(device->GetContext(), block); + Run(context, block); + RecordedUploads(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); From dc1c12a612bf13be74bce1b0bbc22e21d89177c5 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:41:03 -0400 Subject: [PATCH 251/353] feat(recompiler): read the scalar aperture sources as values Reading src_shared_base, src_shared_limit, src_private_base or src_private_limit (scalar source codes 235-238) refused the shader since #1940, because their values were unknown. They now read as the 64-bit apertures that the flat routing from #1190 classifies: a base is the aperture's high dword over a low dword of 0, a limit is the same high dword over 0xffffffff (shared 0x80000000, private 0x70000000). A 64-bit read (s_mov_b64, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. A flat pointer built from the high dword therefore reaches LDS or scratch, and 64-bit compares against the limits order as on the hardware. The flat routing and the reads share one definition of the two high dwords. Sources: LLVM SIRegisterInfo.td (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and getSegmentAperture in AMDGPULegalizerInfo.cpp and SIISelLowering.cpp (a 64-bit read whose high half is the aperture); the RDNA 2 ISA guide, sections 5.2, 5.8 (SH_MEM_BASES holds 16-bit bases) and 9.3; the KFD's MAKE_LDS_APP_LIMIT (limit = base | 0xffffffff); the gfx1030 and gfx1035 readouts on #1190 and #3803. The PS5's two high dwords stay inferred, as the shader technical debt records. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/FlatApertureSources.cpp | 240 ++++++++++++++++++ core/libs/tests/ApertureSources.cpp | 74 ++++-- .../RdnaDecoder/RdnaInstructionDecoder.hpp | 4 + .../src/RdnaInstructionDecoder.cpp | 10 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 4 +- .../src/TranslationContext/ExecMask.cpp | 4 + .../src/TranslationContext/OperandAccess.cpp | 6 +- .../src/TranslationContext/SubwordPacking.cpp | 5 + docs/dev/debt/shader.md | 5 +- 9 files changed, 314 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp new file mode 100644 index 0000000000..21078c04c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp @@ -0,0 +1,240 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +using Words = std::vector; + +constexpr std::uint32_t SharedBase = 235u; +constexpr std::uint32_t SharedLimit = 236u; +constexpr std::uint32_t PrivateBase = 237u; +constexpr std::uint32_t PrivateLimit = 238u; +constexpr std::array Sources{SharedBase, SharedLimit, PrivateBase, PrivateLimit}; +constexpr std::array SourceNames{"src_shared_base", "src_shared_limit", "src_private_base", "src_private_limit"}; +constexpr std::array SourceValues{0x8000000000000000ull, 0x80000000ffffffffull, 0x7000000000000000ull, 0x70000000ffffffffull}; +constexpr std::uint32_t LaneDwords = 32u; +constexpr std::uint32_t LdsDwords = 256u; +constexpr std::uint32_t ScratchDwords = 4u; +constexpr std::uint32_t ScratchOffset = 4u; +constexpr std::uint32_t SharedData = 0x5eed0000u; +constexpr std::uint32_t PrivateData = 0x51de0000u; +constexpr std::uint32_t WaitCnt0 = 0xbf8c0070u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t InlineZero = 128u; +constexpr std::uint32_t InlineOne = 129u; +constexpr std::uint32_t Literal = 255u; +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; +constexpr std::uint32_t CndmaskOp = 0x01u; +constexpr std::uint32_t LshlrevOp = 0x1au; +constexpr std::uint32_t AddNcOp = 0x25u; +constexpr std::uint32_t FlatStoreDword = 0x1cu; +constexpr std::uint32_t FlatLoadDword = 0x0cu; +constexpr std::uint32_t FlatSegment = 0u; +constexpr std::uint32_t ScratchSegment = 1u; +constexpr std::uint32_t CmpLeU64 = 0xe3u; +constexpr std::uint32_t CmpGeU64 = 0xe6u; +constexpr std::uint32_t LaneOffset = 1u; +constexpr std::uint32_t SharedPointer = 3u; +constexpr std::uint32_t SharedValue = 5u; +constexpr std::uint32_t SharedFlatRead = 6u; +constexpr std::uint32_t PrivatePointer = 7u; +constexpr std::uint32_t PrivateValue = 9u; +constexpr std::uint32_t LdsRead = 10u; +constexpr std::uint32_t ScratchRead = 11u; +constexpr std::uint32_t EdgePointer = 12u; +constexpr std::uint32_t PastPointer = 14u; +constexpr std::uint32_t Result = 16u; +constexpr std::uint32_t PrivateFlatRead = 17u; +constexpr std::uint32_t One = 20u; +constexpr std::uint32_t NarrowReads = 8u; +constexpr std::uint32_t WideReads = 12u; +constexpr std::uint32_t PastHigh = 20u; + +struct Compare { + std::uint32_t op; + std::uint32_t source; + std::uint32_t pointer; + std::uint32_t expected; + const char* name; +}; + +constexpr std::array Compares{{ + {CmpLeU64, SharedBase, SharedPointer, 1u, "src_shared_base <= shared pointer"}, + {CmpGeU64, SharedLimit, SharedPointer, 1u, "src_shared_limit >= shared pointer"}, + {CmpLeU64, SharedBase, PrivatePointer, 0u, "src_shared_base <= private pointer"}, + {CmpLeU64, PrivateBase, PrivatePointer, 1u, "src_private_base <= private pointer"}, + {CmpGeU64, PrivateLimit, PrivatePointer, 1u, "src_private_limit >= private pointer"}, + {CmpGeU64, PrivateLimit, SharedPointer, 0u, "src_private_limit >= shared pointer"}, + {CmpGeU64, SharedLimit, EdgePointer, 1u, "src_shared_limit >= src_shared_limit"}, + {CmpGeU64, SharedLimit, PastPointer, 0u, "src_shared_limit >= the first address past it"}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t ScalarMove(std::uint32_t opcode, std::uint32_t sdst, std::uint32_t source) { + return 0xbe800000u | (sdst << 16u) | (opcode << 8u) | source; +} + +std::uint32_t VectorMove(std::uint32_t vdst, std::uint32_t source) { + return 0x7e000200u | (vdst << 17u) | source; +} + +std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t vsrc1) { + return (opcode << 25u) | (vdst << 17u) | (vsrc1 << 9u) | src0; +} + +void EmitFlat(Words& code, std::uint32_t segment, std::uint32_t op, std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + code.push_back(0xdc000000u | (op << 18u) | (segment << 14u)); + code.push_back((vdst << 24u) | (0x7du << 16u) | (data << 8u) | address); +} + +void EmitOutputStore(Words& code, std::uint32_t vdata, std::uint32_t slot) { + code.push_back(0xe0701000u | (slot * 4u)); + code.push_back(0x80000000u | LaneOffset | (vdata << 8u)); +} + +Words Kernel() { + Words code{Vop2(LshlrevOp, LaneOffset, 135u, 0u)}; + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(ScalarMove(MovB32, NarrowReads + index, Sources[index])); + code.push_back(ScalarMove(MovB64, WideReads + index * 2u, Sources[index])); + } + for (std::uint32_t index = 0; index < 12u; ++index) { + code.push_back(VectorMove(Result, NarrowReads + index)); + EmitOutputStore(code, Result, index); + } + code.push_back(Vop2(LshlrevOp, SharedPointer, 130u, 0u)); + code.push_back(VectorMove(SharedPointer + 1u, WideReads + 1u)); + code.push_back(Vop2(AddNcOp, SharedValue, Literal, 0u)); + code.push_back(SharedData); + code.push_back(VectorMove(PrivatePointer, InlineZero + ScratchOffset)); + code.push_back(VectorMove(PrivatePointer + 1u, WideReads + 5u)); + code.push_back(Vop2(AddNcOp, PrivateValue, Literal, 0u)); + code.push_back(PrivateData); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, SharedValue, SharedPointer); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, PrivateValue, PrivatePointer); + code.push_back(WaitCnt0); + code.push_back(0xd8d80000u); + code.push_back((LdsRead << 24u) | SharedPointer); + EmitFlat(code, ScratchSegment, FlatLoadDword, ScratchRead, 0u, PrivatePointer); + EmitFlat(code, FlatSegment, FlatLoadDword, SharedFlatRead, 0u, SharedPointer); + EmitFlat(code, FlatSegment, FlatLoadDword, PrivateFlatRead, 0u, PrivatePointer); + code.push_back(WaitCnt0); + EmitOutputStore(code, LdsRead, 12u); + EmitOutputStore(code, ScratchRead, 13u); + EmitOutputStore(code, SharedFlatRead, 14u); + EmitOutputStore(code, PrivateFlatRead, 15u); + code.push_back(VectorMove(EdgePointer, WideReads + 2u)); + code.push_back(VectorMove(EdgePointer + 1u, WideReads + 3u)); + code.push_back(0x80000000u | (PastHigh << 16u) | (InlineOne << 8u) | (WideReads + 3u)); + code.push_back(VectorMove(PastPointer, InlineZero)); + code.push_back(VectorMove(PastPointer + 1u, PastHigh)); + code.push_back(VectorMove(One, InlineOne)); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + const auto& compare = Compares[index]; + code.push_back(0x7c000000u | (compare.op << 17u) | (compare.pointer << 9u) | compare.source); + code.push_back(Vop2(CndmaskOp, Result, InlineZero, One)); + EmitOutputStore(code, Result, 16u + index); + } + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(0xd5010000u | Result); + code.push_back(InlineZero | (InlineOne << 9u) | (Sources[index] << 18u)); + EmitOutputStore(code, Result, 24u + index); + } + code.push_back(EndProgram); + return code; +} + +void Expect(const std::vector& output, std::uint32_t waveSize, std::uint32_t lane, std::uint32_t slot, std::uint32_t expected, const std::string& what) { + const auto read = output[lane * LaneDwords + slot]; + Require(read == expected, "flat aperture sources wave" + std::to_string(waveSize) + ": lane " + std::to_string(lane) + " " + what + " read " + Hex(read) + ", expected " + Hex(expected)); +} + +void Check(const std::vector& output, std::uint32_t waveSize) { + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + const auto value = SourceValues[index]; + const std::string name = SourceNames[index]; + Expect(output, waveSize, lane, index, static_cast(value), "s_mov_b32 of " + name); + Expect(output, waveSize, lane, 4u + index * 2u, static_cast(value), "s_mov_b64 of " + name + " low"); + Expect(output, waveSize, lane, 5u + index * 2u, static_cast(value >> 32u), "s_mov_b64 of " + name + " high"); + const auto maskBit = waveSize == 32u ? (static_cast(value) >> lane) & 1u : static_cast(value >> lane) & 1u; + Expect(output, waveSize, lane, 24u + index, maskBit, "v_cndmask_b32 with " + name + " as the lane mask"); + } + Expect(output, waveSize, lane, 12u, SharedData + lane, "ds_read_b32 after a flat store through src_shared_base"); + Expect(output, waveSize, lane, 13u, PrivateData + lane, "scratch_load_dword after a flat store through src_private_base"); + Expect(output, waveSize, lane, 14u, SharedData + lane, "flat_load_dword through src_shared_base"); + Expect(output, waveSize, lane, 15u, PrivateData + lane, "flat_load_dword through src_private_base"); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + Expect(output, waveSize, lane, 16u + index, Compares[index].expected, Compares[index].name); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + std::vector output(waveSize * LaneDwords, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto descriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + const auto code = Kernel(); + const std::span codeSpan(code); + const std::array memory{{{reinterpret_cast(codeSpan.data()), std::as_bytes(codeSpan)}}}; + ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + compute.scratchDwords = ScratchDwords; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(codeSpan.data()), codeSpan, 0, {}}, + {waveSize, 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(codeSpan.data())); + device.WaitIdle(); + Check(output, waveSize); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, 32u); + if (device->Target().subgroupSize >= 32u) { + Run(*device, 64u); + } else { + std::printf("skipped wave64, subgroup size %u cannot hold a wave64 in two lanes\n", device->Target().subgroupSize); + } + std::puts("flat aperture sources tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/tests/ApertureSources.cpp b/core/libs/tests/ApertureSources.cpp index f7e05eda1c..30fa83ebe4 100644 --- a/core/libs/tests/ApertureSources.cpp +++ b/core/libs/tests/ApertureSources.cpp @@ -4,6 +4,7 @@ #include "Translation/TranslationContext.hpp" #include #include +#include #include #include @@ -15,40 +16,36 @@ struct ApertureSource { std::uint32_t code; RdnaOperandKind kind; const char* name; + std::uint32_t low; + std::uint32_t high; }; const std::array ApertureSources{{ - {235u, RdnaOperandKind::SrcSharedBase, "src_shared_base"}, - {236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit"}, - {237u, RdnaOperandKind::SrcPrivateBase, "src_private_base"}, - {238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit"}, + {235u, RdnaOperandKind::SrcSharedBase, "src_shared_base", 0x00000000u, 0x80000000u}, + {236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit", 0xffffffffu, 0x80000000u}, + {237u, RdnaOperandKind::SrcPrivateBase, "src_private_base", 0x00000000u, 0x70000000u}, + {238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit", 0xffffffffu, 0x70000000u}, }}; +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; + void Require(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); } -// The refusal must name the source it refused: a bare "not modelled" cannot be told from a decode gap. -template -void ExpectRefusal(const char* source, Fn translate) { - try { - translate(); - } catch (const std::exception& error) { - if (std::string(error.what()).find(source) == std::string::npos) { - throw std::runtime_error(std::string("the refusal does not name ") + source + ": " + error.what()); - } - return; - } - throw std::runtime_error(std::string("reading ") + source + " was accepted"); +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; } -// s_mov_b32 s0, : SOP1, format 0x7d, opcode 0x03, the source code in the low byte. -RdnaInstruction ScalarMove(std::uint32_t source) { - const std::array words{0x80000000u | (0x7du << 23u) | (0x03u << 8u) | source, 0u}; +RdnaInstruction ScalarMove(std::uint32_t opcode, std::uint32_t source) { + const std::array words{0x80000000u | (0x7du << 23u) | (opcode << 8u) | source, 0u}; return DecodeRdnaSop1(0u, words, 0u); } -void Translate(const RdnaInstruction& instruction) { +std::map TranslateWrites(const RdnaInstruction& instruction) { IrProgram program; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -56,6 +53,14 @@ void Translate(const RdnaInstruction& instruction) { program.Metadata().blockInfo.resize(1); TranslationContext context(program, block, 256); context.TranslateInstruction(instruction); + std::map writes; + for (const IrValue* value : block.Instructions()) { + if (value->Opcode() != IrOpcode::SetScalarRegister) continue; + const IrValue* written = value->Argument(1u)->Resolve(); + Require(written->HasImmediate(), "the write of s" + std::to_string(value->Argument(0u)->Register().index) + " is not a constant"); + writes[value->Argument(0u)->Register().index] = written->ImmediateU32(); + } + return writes; } void CheckDecode() { @@ -68,14 +73,27 @@ void CheckDecode() { } } -// Translating the move reads the source as a value, which is the path a title that carries the -// aperture into an address does not take and one that reads it does. -void CheckReadRefuses() { +void CheckWord(const std::map& writes, std::uint32_t reg, std::uint32_t expected, const std::string& read) { + const auto found = writes.find(reg); + Require(found != writes.end(), read + " did not write s" + std::to_string(reg)); + Require(found->second == expected, read + " wrote " + Hex(found->second) + " to s" + std::to_string(reg) + ", expected " + Hex(expected)); +} + +void CheckReadValues() { for (const auto& source : ApertureSources) { - const RdnaInstruction move = ScalarMove(source.code); - Require(move.op == RdnaOpcode::SMovB32, "the scalar move decoded to another opcode"); - Require(move.source0.kind == source.kind, "the scalar move decodes source0 as the wrong operand kind"); - ExpectRefusal(source.name, [&] { Translate(move); }); + const RdnaInstruction narrow = ScalarMove(MovB32, source.code); + Require(narrow.op == RdnaOpcode::SMovB32, "the 32-bit scalar move decoded to another opcode"); + Require(narrow.source0.kind == source.kind, "the 32-bit scalar move decodes source0 as the wrong operand kind"); + const auto narrowWrites = TranslateWrites(narrow); + CheckWord(narrowWrites, 0u, source.low, std::string("s_mov_b32 of ") + source.name); + Require(narrowWrites.size() == 1u, std::string("s_mov_b32 of ") + source.name + " wrote more than one register"); + const RdnaInstruction wide = ScalarMove(MovB64, source.code); + Require(wide.op == RdnaOpcode::SMovB64, "the 64-bit scalar move decoded to another opcode"); + Require(wide.source0.kind == source.kind, "the 64-bit scalar move decodes source0 as the wrong operand kind"); + const auto wideWrites = TranslateWrites(wide); + CheckWord(wideWrites, 0u, source.low, std::string("s_mov_b64 of ") + source.name); + CheckWord(wideWrites, 1u, source.high, std::string("s_mov_b64 of ") + source.name); + Require(wideWrites.size() == 2u, std::string("s_mov_b64 of ") + source.name + " wrote more than two registers"); } } @@ -83,7 +101,7 @@ void CheckReadRefuses() { int main() { CheckDecode(); - CheckReadRefuses(); + CheckReadValues(); std::puts("aperture sources tests passed"); return 0; } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index a03a46841e..9456f38cd1 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -8,6 +8,9 @@ namespace ShaderRecompiler { +inline constexpr std::uint32_t SharedApertureHigh = 0x80000000u; +inline constexpr std::uint32_t PrivateApertureHigh = 0x70000000u; + class RdnaInstructionDecoder { public: [[nodiscard]] RdnaProgram Decode(std::span code) const; @@ -23,6 +26,7 @@ void DecodeRdnaProgram(std::span code, RdnaProgram& program RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaScalarDestination(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaVectorGpr(std::uint32_t reg); +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand); void ReadRdnaLiteralOperands(std::span code, std::uint32_t wordIndex, RdnaInstruction& instruction); void SetRdnaRawWords(RdnaInstruction& instruction, std::span code, std::uint32_t wordIndex, std::uint32_t wordCount); void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily family, std::uint32_t opcodeId, const char* reason); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index fc5e2b9d63..abdb151c23 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -300,6 +300,16 @@ void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily fami throw std::invalid_argument("unsupported RDNA instruction, family=" + std::to_string(static_cast(family)) + " opcode=" + toHexString(opcodeId) + " reason=" + reason + " pc=" + toHexString(instruction.programCounter)); } +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::SrcSharedBase: return std::uint64_t{SharedApertureHigh} << 32u; + case RdnaOperandKind::SrcSharedLimit: return (std::uint64_t{SharedApertureHigh} << 32u) | 0xffffffffu; + case RdnaOperandKind::SrcPrivateBase: return std::uint64_t{PrivateApertureHigh} << 32u; + case RdnaOperandKind::SrcPrivateLimit: return (std::uint64_t{PrivateApertureHigh} << 32u) | 0xffffffffu; + default: throw std::invalid_argument("RdnaApertureSourceValue: " + RdnaOperandToString(operand) + " is not a scalar aperture source"); + } +} + std::string RdnaOperandToString(const RdnaOperand& operand) { switch (operand.kind) { case RdnaOperandKind::None: return "none"; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 64ae0cd782..622fbc68bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -3,6 +3,7 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -336,9 +337,6 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -constexpr std::uint32_t SharedApertureHigh = 0x80000000u; -constexpr std::uint32_t PrivateApertureHigh = 0x70000000u; - bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp index 29e92ea915..0b0fd6ae8e 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -53,6 +53,10 @@ IrU1 TranslationContext::readMask(const RdnaOperand& operand) { case RdnaOperandKind::Scc: return IrU1(ir.GetScc()); case RdnaOperandKind::VccZ: return IrU1(ir.LogicalNot(ir.GetVcc())); case RdnaOperandKind::ExecZ: return IrU1(ir.LogicalNot(ir.GetExec())); + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return threadBit(readU32Pair(operand)); default: return IrU1(ir.INotEqual(readRawU32(operand).Value(), ir.Constant(0u))); } } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 27b6e20f80..f82197547f 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -448,8 +448,7 @@ IrU32 TranslationContext::readScalarCode(std::uint32_t code) { case 235u: case 236u: case 237u: - case 238u: - throw std::runtime_error("TranslationContext::readScalarCode: the scalar aperture source " + RdnaOperandToString(DecodeRdnaScalarSource(code, 0u)) + " is not modelled"); + case 238u: return readRawU32(DecodeRdnaScalarSource(code, 0u)); default: return IrU32(ir.Constant(0u)); } } @@ -482,8 +481,7 @@ IrU32 TranslationContext::readRawU32(const RdnaOperand& operand) { case RdnaOperandKind::SrcSharedBase: case RdnaOperandKind::SrcSharedLimit: case RdnaOperandKind::SrcPrivateBase: - case RdnaOperandKind::SrcPrivateLimit: - throw std::runtime_error("TranslationContext::readRawU32: the scalar aperture source " + RdnaOperandToString(operand) + " is not modelled"); + case RdnaOperandKind::SrcPrivateLimit: return IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand)))); default: throw std::runtime_error("TranslationContext::readRawU32 invalid decoded operand used as a raw U32 source"); } } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index 12a55ce576..cf6c3a789f 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -1,4 +1,5 @@ #include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -19,6 +20,10 @@ std::array TranslationContext::readU32Pair(const RdnaOperand& operand) const auto bits = std::bit_cast(static_cast(std::bit_cast(operand.value))); return {IrU32(ir.Constant(static_cast(bits))), IrU32(ir.Constant(static_cast(bits >> 32u)))}; } + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return {readU32(operand), IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand) >> 32u)))}; default: return {readU32(operand), readU32(offsetOperand(operand, 1u))}; } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7163d55302..1cb8efb90f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -50,8 +50,7 @@ - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples -- [Scalar aperture base sources](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) - `src_shared_base`, `src_shared_limit`, `src_private_base` and `src_private_limit` (scalar source codes 235 to 238) are not modelled: decoding throws `unsupported scalar source operand code` with the code and the program counter, so a shader that reads one does not compile. Operand 235 (`src_shared_base`) was reported on [#1182](https://github.com/boykopovar/AnyPS5/pull/1182) from PPSA26344; the flat aperture routing does not read the base, so a shader that builds its own flat addresses from it stops at decode. -- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES +- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes. @@ -106,7 +105,7 @@ - [DS src2 operations](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981). - [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, compare, hash, store) throws, apart from the overwrites and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. -- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not +- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. From babc68df7454fd93869e3ceff258374caa16f3b8 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:08:25 -0400 Subject: [PATCH 252/353] feat(libScePad): declare the unknown import zBsgF0q8DIM PPSA03671 fails to load because no library exports NID zBsgF0q8DIM from libScePad. The NID database has no name for it and no candidate pad name hashes to it, so it is exported under its NID as scePadUnknown_zBsgF0q8DIM, declared without parameters, and throws when called. Adds an Unknown function info entry to the libScePad debt file. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePad/Export.cpp | 6 ++++++ docs/dev/debt/libScePad.md | 1 + 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index db18fe77d6..bc8838e917 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -319,4 +319,10 @@ int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { return 0; } +APS5_EXPORT("zBsgF0q8DIM", scePadUnknown_zBsgF0q8DIM); +int APS5_VABI scePadUnknown_zBsgF0q8DIM() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 8cfd5341d6..04435c2849 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -8,3 +8,4 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 0 to 3 are accepted and stored; the meaning of mode 3 is unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled +- [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown From f0f920a66ecce2ab94af89f1539fa980fd656da7 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:57:12 -0400 Subject: [PATCH 253/353] feat(recompiler): reuse pure IR values recomputed in dominated blocks The front end rebuilds operand flushes, NaN checks and bitcasts for every use of a register, and nothing merged them when SPIRV-Tools is off. A dominator-scoped value numbering pass before SPIR-V emission replaces a pure value with an earlier identical one in the same or a dominating block. Only operations that are functions of their operands are merged; loads, stores, atomics, barriers, phis and cross-lane reads are left alone. A block the entry does not reach throws. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 + .../tests/RedundantValueElimination.cpp | 447 ++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../Optimization/RedundantValueEliminator.hpp | 20 + .../src/RedundantValueEliminator.cpp | 276 +++++++++++ core/shader/recompiler/Recompiler.cpp | 3 + 6 files changed, 760 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp create mode 100644 core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp create mode 100644 core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3fdab7d9a8..2ea55af59c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -618,6 +618,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_redundant_value_elimination_tests EXCLUDE_FROM_ALL tests/RedundantValueElimination.cpp) +target_link_libraries(agc_redundant_value_elimination_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_redundant_value_elimination_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_redundant_value_elimination_tests) +if(BUILD_TESTING) + set_target_properties(agc_redundant_value_elimination_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_redundant_value_elimination COMMAND agc_redundant_value_elimination_tests) + if(WIN32) + set_tests_properties(agc_redundant_value_elimination PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp) target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp new file mode 100644 index 0000000000..55fb0a5297 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -0,0 +1,447 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/RedundantValueEliminator.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + + Builder() { + block = &newBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& floatConstant(std::uint32_t bits) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::F32); + created.SetImmediateU32(bits); + return created; + } + + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0u) { + auto& created = program.CreateValue(opcode, type, flags); + for (auto* argument : arguments) created.AddArgument(argument); + block->AppendInstruction(&created); + return created; + } + + IrValue& unknown() { + return emit(IrOpcode::LaneId, IrType::U32, {}); + } + + IrValue& flush(IrValue& bits) { + auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)}); + auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)}); + auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)}); + return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits}); + } + + void keep(IrValue& value) { + static_cast(emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + } + + std::uint32_t eliminate() { + return RedundantValueEliminator{}.Eliminate(program).replacedValues; + } +}; + +bool expect(const char* name, bool condition) { + if (!condition) std::fprintf(stderr, "%s\n", name); + return condition; +} + +bool run(const char* name, const std::function& test) { + try { + return expect(name, test()); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + return false; + } +} + +bool AllUsed(const std::vector& values) { + for (const auto* value : values) { + if (!value->HasUses()) return false; + } + return true; +} + +bool PureSpirv(std::uint32_t opcode) { + switch (opcode) { + case spv::OpBitcast: + case spv::OpUConvert: + case spv::OpSConvert: + case spv::OpFConvert: + case spv::OpConvertFToU: + case spv::OpConvertFToS: + case spv::OpConvertUToF: + case spv::OpConvertSToF: + case spv::OpCompositeConstruct: + case spv::OpCompositeExtract: + case spv::OpSNegate: + case spv::OpFNegate: + case spv::OpIAdd: + case spv::OpISub: + case spv::OpIMul: + case spv::OpFAdd: + case spv::OpFSub: + case spv::OpFMul: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpShiftRightArithmetic: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpBitwiseXor: + case spv::OpNot: + case spv::OpBitFieldInsert: + case spv::OpBitFieldUExtract: + case spv::OpBitFieldSExtract: + case spv::OpBitReverse: + case spv::OpBitCount: + case spv::OpSelect: + case spv::OpIEqual: + case spv::OpINotEqual: + case spv::OpULessThan: + case spv::OpULessThanEqual: + case spv::OpUGreaterThan: + case spv::OpUGreaterThanEqual: + case spv::OpSLessThan: + case spv::OpSLessThanEqual: + case spv::OpSGreaterThan: + case spv::OpSGreaterThanEqual: + case spv::OpFOrdEqual: + case spv::OpFOrdNotEqual: + case spv::OpFOrdLessThan: + case spv::OpFOrdGreaterThan: + case spv::OpFOrdLessThanEqual: + case spv::OpFOrdGreaterThanEqual: + case spv::OpFUnordEqual: + case spv::OpFUnordNotEqual: + case spv::OpFUnordLessThan: + case spv::OpFUnordGreaterThan: + case spv::OpFUnordLessThanEqual: + case spv::OpFUnordGreaterThanEqual: + case spv::OpIsNan: + case spv::OpLogicalAnd: + case spv::OpLogicalOr: + case spv::OpLogicalNot: + case spv::OpLogicalEqual: + case spv::OpLogicalNotEqual: + return true; + default: + return false; + } +} + +std::size_t RepeatedInBlock(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::size_t repeated = 0; + std::set> seen; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpLabel) seen.clear(); + if (PureSpirv(opcode) && count >= 3) { + std::vector key{opcode, words[offset + 1]}; + key.insert(key.end(), words.begin() + static_cast(offset + 3), words.begin() + static_cast(offset + count)); + if (!seen.insert(std::move(key)).second) ++repeated; + } + offset += count; + } + return repeated; +} + +std::vector Recompiled(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array capabilities{1u, 61u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +} + +int main() { + bool passed = true; + + passed &= run("a value recomputed from the same operands in one block is replaced by the first", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.flush(source); + auto& second = b.flush(source); + b.keep(first); + b.keep(second); + return b.eliminate() == 4u && !second.HasUses() && first.UseCount() == 2u; + }); + + passed &= run("values that differ in an immediate, an operand or the opcode stay", [] { + Builder b; + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& masked = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + auto& otherMask = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x80000000u)}); + auto& otherSource = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&other, &b.constant(0x7f800000u)}); + auto& otherOpcode = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + b.keep(masked); + b.keep(otherMask); + b.keep(otherSource); + b.keep(otherOpcode); + return b.eliminate() == 0u && AllUsed({&masked, &otherMask, &otherSource, &otherOpcode}); + }); + + passed &= run("values that differ only in flags, the result type or the type of an immediate stay", [] { + Builder b; + auto& source = b.unknown(); + auto& plain = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}); + auto& flagged = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}, 1u); + auto& otherType = b.emit(IrOpcode::BitwiseAnd32, IrType::F32, {&source, &b.constant(0x3f800000u)}); + auto& floatImmediate = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.floatConstant(0x3f800000u)}); + b.keep(plain); + b.keep(flagged); + b.keep(otherType); + b.keep(floatImmediate); + return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); + }); + + passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] { + Builder b; + auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& plusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x00000000u)}); + auto& minusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x80000000u)}); + auto& quiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& sameQuiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& otherPayload = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00001u)}); + for (auto* value : {&plusZero, &minusZero, &quiet, &sameQuiet, &otherPayload}) b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {value})); + return b.eliminate() == 2u && !sameQuiet.HasUses() && AllUsed({&quiet, &plusZero, &minusZero, &otherPayload}); + }); + + passed &= run("a value recomputed in a dominated block is replaced, one in a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + b.keep(first); + b.block = &then; + auto& dominated = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + auto& thenOnly = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(dominated); + b.keep(thenOnly); + b.block = &otherwise; + auto& sibling = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(sibling); + b.block = &merge; + auto& merged = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(merged); + return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); + }); + + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& source = b.unknown(); + auto& other = b.unknown(); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + b.keep(inHeader); + b.block = &body; + auto& inBody = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + auto& bodyOnly = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(inBody); + b.keep(bodyOnly); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(afterLoop); + return b.eliminate() == 1u && !inBody.HasUses() && inHeader.UseCount() == 2u && AllUsed({&bodyOnly, &afterLoop}); + }); + + passed &= run("a branch condition that is replaced names the value that stays", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + auto& second = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&first, &source, &b.constant(1u)})); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&second, &b.constant(1u), &source})); + BlockInfo info; + info.condition = &second; + b.program.Metadata().blockInfo.push_back(info); + return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first; + }); + + passed &= run("lane reads are not merged", [] { + Builder b; + auto& first = b.unknown(); + auto& second = b.unknown(); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && second.HasUses(); + }); + + passed &= run("cross-lane reads with the same operands are not merged", [] { + Builder b; + auto& source = b.unknown(); + std::vector values; + for (int copy = 0; copy < 2; ++copy) { + values.push_back(&b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::ReadLane, IrType::U32, {&source, &b.constant(3u)})); + values.push_back(&b.emit(IrOpcode::DppMoveU32, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::SwizzleU32, IrType::U32, {&source, &b.constant(0x1fu), &b.truth()})); + } + for (auto* value : values) b.keep(*value); + return b.eliminate() == 0u && AllUsed(values); + }); + + passed &= run("loads of one address are not merged, with no store, a store or a barrier between them", [] { + Builder b; + auto& address = b.unknown(); + std::vector loads; + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::StoreShared, IrType::Void, {&address, &b.constant(7u)})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::Barrier, IrType::Void, {})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + for (auto* load : loads) b.keep(*load); + return b.eliminate() == 0u && AllUsed(loads); + }); + + passed &= run("atomics with the same operands are not merged", [] { + Builder b; + auto& address = b.unknown(); + auto& first = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + auto& second = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("phis with the same incoming values are not merged", [] { + Builder b; + IrBlock& left = b.newBlock(); + IrBlock& right = b.newBlock(); + IrBlock& merge = b.newBlock(); + b.block->AddBranch(&left); + b.block->AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& one = b.unknown(); + auto& two = b.unknown(); + b.block = &merge; + auto& first = b.emit(IrOpcode::Phi, IrType::U32, {}); + auto& second = b.emit(IrOpcode::Phi, IrType::U32, {}); + for (auto* phi : {&first, &second}) { + phi->AddPhiOperand(&left, &one); + phi->AddPhiOperand(&right, &two); + } + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("float operations with the same operands in the same order are merged", [] { + Builder b; + auto& lhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& rhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& first = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& second = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& swapped = b.emit(IrOpcode::FPMul32, IrType::F32, {&rhs, &lhs}); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&first})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&second})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&swapped})); + return b.eliminate() == 2u && !second.HasUses() && swapped.HasUses(); + }); + + passed &= run("a block that the entry does not reach throws", [] { + Builder b; + static_cast(b.newBlock()); + try { + static_cast(b.eliminate()); + } catch (const std::runtime_error&) { + return true; + } + return false; + }); + + passed &= run("a wave64 shader that reads one register in several float operations computes nothing twice in a block", [] { + const std::array code{ + 0x10020100u, + 0x10040300u, + 0x10060500u, + 0x06020503u, + 0xe0700000u, 0x80000100u, + 0xbf810000u, + }; + const auto module = Recompiled(code); + const auto repeated = RepeatedInBlock(module); + if (repeated != 0u) std::fprintf(stderr, " %zu pure instructions repeat an earlier one in their block of the %zu-word module\n", repeated, module.size()); + return repeated == 0u; + }); + + return passed ? 0 : 1; +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index d4ff8cefaa..f6b13512b0 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -109,6 +109,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ShaderInfoCollector.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SharedMemoryBarrierInserter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DenormalFlushEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/RedundantValueEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtAddressArithmetic.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDiagnostics.cpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp new file mode 100644 index 0000000000..910c3499db --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct RedundantValueEliminationStats { + std::uint32_t replacedValues = 0; +}; + +class RedundantValueEliminator { +public: + [[nodiscard]] RedundantValueEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp new file mode 100644 index 0000000000..a580617abd --- /dev/null +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -0,0 +1,276 @@ +#include "Optimization/RedundantValueEliminator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Recomputable(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU16F16: + case IrOpcode::BitCastF16U16: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertU16U32: + case IrOpcode::ConvertU32U16: + case IrOpcode::ConvertU8U32: + case IrOpcode::ConvertU32U8: + case IrOpcode::ConvertF32F16: + case IrOpcode::ConvertF16F32: + case IrOpcode::ConvertS32F32: + case IrOpcode::ConvertU32F32: + case IrOpcode::ConvertF32S32: + case IrOpcode::ConvertF32U32: + case IrOpcode::CompositeConstructU64: + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::CompositeConstructU32x3: + case IrOpcode::CompositeConstructF32x2: + case IrOpcode::CompositeConstructU32x4: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::CompositeExtractU32x3: + case IrOpcode::CompositeExtractU32x4: + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::FPSaturate32: + case IrOpcode::BitFieldInsert: + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: + case IrOpcode::SelectU32: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + case IrOpcode::IMul32: + case IrOpcode::IAdd64: + case IrOpcode::ISub64: + case IrOpcode::IMul64: + case IrOpcode::SMulHi: + case IrOpcode::UMulHi: + case IrOpcode::IAbs32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::BitwiseNot32: + case IrOpcode::BitReverse32: + case IrOpcode::BitCount32: + case IrOpcode::BitCount64: + case IrOpcode::FindUMsb32: + case IrOpcode::FindUMsb64: + case IrOpcode::FindILsb32: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::SMax32: + case IrOpcode::UMax32: + case IrOpcode::SLessThan32: + case IrOpcode::SLessThan64: + case IrOpcode::ULessThan32: + case IrOpcode::ULessThan64: + case IrOpcode::IEqual32: + case IrOpcode::IEqual64: + case IrOpcode::SLessThanEqual32: + case IrOpcode::ULessThanEqual32: + case IrOpcode::SGreaterThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::UGreaterThan64: + case IrOpcode::INotEqual32: + case IrOpcode::INotEqual64: + case IrOpcode::SGreaterThanEqual32: + case IrOpcode::UGreaterThanEqual32: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalXor: + case IrOpcode::LogicalNot: + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: + case IrOpcode::FPOrdLessThan32: + case IrOpcode::FPUnordLessThan32: + case IrOpcode::FPOrdGreaterThan32: + case IrOpcode::FPUnordGreaterThan32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: + case IrOpcode::FPIsNan32: + case IrOpcode::FPAdd32: + case IrOpcode::FPSub32: + case IrOpcode::FPFma32: + case IrOpcode::FPMad32: + case IrOpcode::FPMul32: + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPFract32: + case IrOpcode::FPRoundEven32: + return true; + default: + return false; + } +} + +struct Operand { + const IrValue* value; + IrType type; + std::uint64_t immediate; + + bool operator==(const Operand& other) const = default; +}; + +struct ValueKey { + IrOpcode opcode; + IrType type; + std::uint64_t flags; + std::vector operands; + + bool operator==(const ValueKey& other) const = default; +}; + +struct ValueKeyHash { + std::size_t operator()(const ValueKey& key) const { + std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; + for (const auto& operand : key.operands) { + hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); + } + return hash; + } +}; + +Operand OperandOf(const IrValue* argument) { + const IrValue* resolved = argument->Resolve(); + if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; + return {resolved, resolved->Type(), 0u}; +} + +std::vector ReversePostOrder(IrBlock& entry) { + std::vector order; + std::unordered_map visited; + std::vector> stack{{&entry, 0u}}; + visited[&entry] = true; + while (!stack.empty()) { + auto& [block, next] = stack.back(); + if (next < block->Successors().size()) { + IrBlock* successor = block->Successors()[next++]; + if (visited.try_emplace(successor, true).second) stack.emplace_back(successor, 0u); + continue; + } + order.push_back(block); + stack.pop_back(); + } + return {order.rbegin(), order.rend()}; +} + +std::vector> DominatorChildren(const std::vector& order, std::unordered_map& index) { + for (std::size_t position = 0; position < order.size(); ++position) index[order[position]] = position; + constexpr std::size_t none = static_cast(-1); + std::vector dominator(order.size(), none); + dominator[0] = 0; + const auto intersect = [&](std::size_t left, std::size_t right) { + while (left != right) { + while (left > right) left = dominator[left]; + while (right > left) right = dominator[right]; + } + return left; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 1; position < order.size(); ++position) { + std::size_t candidate = none; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end() || dominator[found->second] == none) continue; + candidate = candidate == none ? found->second : intersect(candidate, found->second); + } + if (candidate != dominator[position]) { + dominator[position] = candidate; + changed = true; + } + } + } + std::vector> children(order.size()); + for (std::size_t position = 1; position < order.size(); ++position) children[dominator[position]].push_back(order[position]); + return children; +} + +} + +RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { + RedundantValueEliminationStats stats; + std::unordered_map available; + std::vector added; + std::unordered_map replacements; + ValueKey key; + const auto visit = [&](IrBlock& block) { + for (auto* inst : block.Instructions()) { + if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; + key.opcode = inst->Opcode(); + key.type = inst->Type(); + key.flags = inst->Flags(); + key.operands.clear(); + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) key.operands.push_back(OperandOf(inst->Argument(index))); + const auto [found, inserted] = available.try_emplace(key, inst); + if (inserted) { + added.push_back(&found->first); + continue; + } + inst->ReplaceAllUsesWith(found->second); + replacements.emplace(inst, found->second); + ++stats.replacedValues; + } + }; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::vector> stack{{0u, 0u}}; + std::vector scopes{added.size()}; + visit(*order[0]); + while (!stack.empty()) { + auto& [position, next] = stack.back(); + if (next < children[position].size()) { + const auto child = index.at(children[position][next++]); + scopes.push_back(added.size()); + visit(*order[child]); + stack.emplace_back(child, 0u); + continue; + } + while (added.size() > scopes.back()) { + available.erase(available.find(*added.back())); + added.pop_back(); + } + scopes.pop_back(); + stack.pop_back(); + } + const auto replaced = [&](IrValue*& value) { + if (value == nullptr) return; + if (const auto found = replacements.find(value->Resolve()); found != replacements.end()) value = found->second; + }; + for (auto& info : program.Metadata().blockInfo) { + replaced(info.condition); + replaced(info.indirectTarget); + } + for (auto*& read : program.Metadata().dynamicReads) replaced(read); + return stats; +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 46f382ce18..9b3b8d0116 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -32,6 +32,7 @@ #include "Optimization/include/Optimization/HostInterpolationChecker.hpp" #include "Optimization/include/Optimization/MaskedSelectEliminator.hpp" #include "Optimization/include/Optimization/ReadLaneEliminator.hpp" +#include "Optimization/include/Optimization/RedundantValueEliminator.hpp" #include "Optimization/include/Optimization/RequestMemoryView.hpp" #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" @@ -458,6 +459,8 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets); deadCodeEliminator.RemoveIdentities(program); + constexpr RedundantValueEliminator redundantValueEliminator; + static_cast(redundantValueEliminator.Eliminate(program)); deadCodeEliminator.Eliminate(program); constexpr ShaderInfoCollector shaderInfoCollector; From dd3c13ffd7d782319dd15d045729036cf97def9e Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:06:19 -0400 Subject: [PATCH 254/353] feat(agc): execute INDEX_ATTRIBUTES_INDIRECT (PM4 0x91) The driver had no handler for the PFP INDEX_ATTRIBUTES_INDIRECT packet, so a submission with it threw "opcode is not known in the reference". The packet now reads the 16-byte record at attribute_base + attribute_index * 16 when it executes and loads INDEX_BASE, INDEX_BUFFER_SIZE and INDEX_TYPE from it as the direct packets do. The packet encoding follows PAL's BuildIndexAttributesIndirect and the record layout PAL's IndexAttribIndirect (gfx9Chip.h, c5e80007). indirect_mode 1, reserved bits and record index types above 3 throw. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Pm4Opcodes.hpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 4 + .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 46 +++++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 104 +++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 153 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp index e78777f28f..3786ec368b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp @@ -65,6 +65,7 @@ inline constexpr Opcode Opcodes[] = { {0x88, "WAIT_ON_DE_COUNTER_DIFF"}, {0x8D, "DISPATCH_DRAW"}, {0x8E, "GET_LOD_STATS"}, + {0x91, "INDEX_ATTRIBUTES_INDIRECT"}, {0x93, "WAIT_REG_MEM_64"}, {0x9F, "SET_CONTEXT_REG_INDIRECT"}, }; 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 c65419b0ee..1ecbf36a71 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -302,6 +302,10 @@ void Driver::execute(const Submission& submission) { const auto condition = Pm4::ReadCondition(packet); if (condition == 0) nextCursor = submission.conditionalEnds.at(cursor); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f queue 0x%x COND_EXEC at DWORD %zu reads 0x%x at 0x%llx: %s %zu dwords\n", TraceMs(), submission.queue, cursor, condition, static_cast(packet[1] | (static_cast(packet[2]) << 32u)), condition == 0 ? "skips" : "executes", submission.conditionalEnds.at(cursor) - cursor - count); + } else if (opcode == 0x91) { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 2fdd6b930b..3563c24a97 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -198,7 +198,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27: case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x45: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: - case 0x81: case 0x83: case 0x9f: case 0x28: return {}; + case 0x81: case 0x83: case 0x91: case 0x9f: case 0x28: return {}; case 0x24: case 0x25: case 0x2c: case 0x38: if (IndirectDrawsDisabled()) return "graphics draw, shader stages and guest render-target materialization are not implemented"; return {}; @@ -301,6 +301,13 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[5] & ~0x20u) == 0, "unsupported indexed draw flags"); break; case 0x2a: graphics(); size(2); require(packet[1] <= 3, "unsupported index-type modifiers"); break; + case 0x91: + graphics(); + size(4); + require((packet[1] & 1u) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode (addr_offset) is not implemented"); + require((packet[1] & 0xeu) == 0, "INDEX_ATTRIBUTES_INDIRECT attribute base is not 16-byte aligned"); + require((packet[3] >> 16u) == 0, "INDEX_ATTRIBUTES_INDIRECT reserved attribute index bits are set"); + break; case 0x2d: graphics(); size(3); @@ -647,7 +654,7 @@ bool UsesGpuCacheBarrier(std::span packet) { bool AccessesMemory(std::uint32_t header) { switch ((header >> 8u) & 0xffu) { - case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; + case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x91: case 0x9f: return true; default: return false; } } @@ -828,6 +835,40 @@ void ExecuteIndirectRegisters(std::span packet, std::span words; +}; + +IndexAttributes readIndexAttributes(std::span packet) { + require(packet.size() == 4 && ((packet[0] >> 8u) & 0xffu) == 0x91, "expected INDEX_ATTRIBUTES_INDIRECT packet"); + require((packet[1] & 0xfu) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode or reserved base bits are not implemented"); + const auto base = address(packet[1], packet[2]); + const auto offset = static_cast(packet[3] & 0xffffu) * IndexAttributesBytes; + require(offset <= std::numeric_limits::max() - base, "INDEX_ATTRIBUTES_INDIRECT record address overflow"); + IndexAttributes attributes{base + offset, {}}; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::IndirectArguments); + GuestMemory::Read(attributes.address, std::as_writable_bytes(std::span(attributes.words)), IndexAttributesBytes); + return attributes; +} + +void executeIndexAttributes(const IndexAttributes& attributes, QueueState& queue) { + if (attributes.words[3] > 3) { + char what[160]; + std::snprintf(what, sizeof(what), "INDEX_ATTRIBUTES_INDIRECT record at 0x%llx has index type 0x%x (index-type modifiers are not implemented)", static_cast(attributes.address), attributes.words[3]); + throw std::runtime_error(what); + } + queue.indexBase = address(attributes.words[0], attributes.words[1]); + queue.indexBufferSize = attributes.words[2]; + queue.indexType = attributes.words[3]; +} + +} + void Execute(std::span packet, QueueState& queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; switch (opcode) { @@ -874,6 +915,7 @@ void Execute(std::span packet, QueueState& queue) { case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0]))); case 0x26: queue.indexBase = address(packet[1], packet[2]); return; case 0x2a: queue.indexType = packet[1]; return; + case 0x91: executeIndexAttributes(readIndexAttributes(packet), queue); return; case 0x2f: queue.instanceCount = packet[1]; return; case 0x63: case 0x64: case 0x9f: ExecuteIndirectRegisters(packet, ReadIndirectRegisters(packet), queue); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 490ba22a37..a939a4f433 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -66,7 +66,7 @@ void testCatalog() { const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]); if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str()); } - check(values.size() == 55, "reference opcode catalog is incomplete"); + check(values.size() == 56, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); const std::array, 11> custom{{ {5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"}, @@ -352,6 +352,89 @@ void testIndirectDraw() { expectFailure([&] { AgcDriver::Pm4::ReadDrawCount(*local.indirect); }, "count address"); } +bool sameDraw(const AgcDriver::Pm4::DrawParameters& left, const AgcDriver::Pm4::DrawParameters& right) { + return left.indexAddress == right.indexAddress && left.indexCount == right.indexCount && left.indexSize == right.indexSize && left.instanceCount == right.instanceCount && + left.flags == right.flags && left.indexed == right.indexed && left.firstVertex == right.firstVertex && left.indirect.has_value() == right.indirect.has_value(); +} + +void testIndexAttributesIndirect() { + check(AgcDriver::Pm4::Name(0xc0029100u) == "INDEX_ATTRIBUTES_INDIRECT" && AgcDriver::Pm4::UnsupportedReason(0xc0029100u).empty(), "INDEX_ATTRIBUTES_INDIRECT is not executed"); + check(AgcDriver::Pm4::AccessesMemory(0xc0029100u), "INDEX_ATTRIBUTES_INDIRECT must synchronize guest memory"); + alignas(4) static std::array indices{}; + const auto indexAddress = reinterpret_cast(indices.data()) + 4; + alignas(16) static std::array records{}; + records.fill(0xdeadbeefu); + records[4] = static_cast(indexAddress); + records[5] = static_cast(indexAddress >> 32u); + records[6] = 12; + records[7] = 1; + const auto packetFor = [](const void* base, std::uint32_t index) { return makePacket(0x91, {low(base), high(base), index}); }; + const auto packet = packetFor(records.data(), 1); + AgcDriver::QueueState loaded; + loaded.indexBase = 0x1234; + loaded.indexBufferSize = 77; + loaded.indexType = 2; + execute(loaded, packet); + check(loaded.indexBase == indexAddress && loaded.indexBufferSize == 12 && loaded.indexType == 1, "INDEX_ATTRIBUTES_INDIRECT did not load INDEX_BASE, INDEX_BUFFER_SIZE and INDEX_TYPE from its record"); + AgcDriver::QueueState direct; + execute(direct, makePacket(0x26, {static_cast(indexAddress), static_cast(indexAddress >> 32u)})); + execute(direct, makePacket(0x13, {12})); + execute(direct, makePacket(0x2a, {1})); + const auto offsetDraw = makePacket(0x35, {12, 1, 5, 0}); + const auto fromRecord = AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); + check(sameDraw(fromRecord, AgcDriver::Pm4::ResolveDraw(offsetDraw, direct)) && fromRecord.indexAddress == indexAddress + 4 && fromRecord.indexSize == 4 && fromRecord.indexCount == 5, "an indexed draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + execute(loaded, makePacket(0x11, {1, low(records.data()), high(records.data())})); + execute(direct, makePacket(0x11, {1, low(records.data()), high(records.data())})); + const auto indirectDraw = makePacket(0x25, {0, 0x8c, 0x280, 0}); + const auto indirectFromRecord = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); + check(sameDraw(indirectFromRecord, AgcDriver::Pm4::ResolveDraw(indirectDraw, direct)) && indirectFromRecord.indexAddress == indexAddress && indirectFromRecord.indexCount == 12 && indirectFromRecord.indexSize == 4, "an indexed indirect draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + records[6] = 0x7fffffffu; + execute(loaded, packet); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "guest"); + records[6] = 12; + + static std::size_t flushed = 0; + flushed = 0; + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } reset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + check(address == reinterpret_cast(records.data() + 4) && bytes == 16, "INDEX_ATTRIBUTES_INDIRECT synchronized an unrelated range"); + records[6] = 9; + ++flushed; + }); + execute(loaded, packet); + check(flushed == 1 && loaded.indexBufferSize == 9, "INDEX_ATTRIBUTES_INDIRECT read its record before the GPU work that writes it"); + AgcDriver::GuestMemory::SetFlushHook(nullptr); + records[6] = 12; + + const auto unchanged = [&](const std::vector& rejected, const char* text) { + AgcDriver::QueueState state; + state.indexBase = 0x40; + state.indexBufferSize = 3; + state.indexType = 2; + expectFailure([&] { execute(state, rejected); }, text); + check(state.indexBase == 0x40 && state.indexBufferSize == 3 && state.indexType == 2, "a rejected INDEX_ATTRIBUTES_INDIRECT changed the index state"); + }; + unchanged(makePacket(0x91, {low(records.data()) | 1u, high(records.data()), 1}), "indirect mode"); + unchanged(makePacket(0x91, {low(records.data()) | 4u, high(records.data()), 1}), "16-byte aligned"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data()), 0x10001u}), "reserved attribute index bits"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data())}), "packet size"); + unchanged(packetFor(nullptr, 0), "null or misaligned"); + unchanged(packetFor(reinterpret_cast(0x1000), 0), "guest"); + unchanged(makePacket(0x91, {0xfffffff0u, 0xffffffffu, 1}), "record address overflow"); + expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue"); + records[7] = 4; + unchanged(packet, "has index type 0x4"); + char named[64]; + std::snprintf(named, sizeof(named), "record at 0x%llx", static_cast(reinterpret_cast(records.data() + 4))); + unchanged(packet, named); + records[7] = 3; + execute(loaded, packet); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); }, "unsupported index type"); + records[7] = 1; +} + void testMemory() { AgcDriver::QueueState state; std::array data{0, 0, 0, 0}; @@ -836,6 +919,23 @@ void testRegisterListsReadAtSubmission() { check(std::atomic_ref(done).load() == 1, "register list rewritten after submission was read by the worker"); } +void testIndexAttributesReadAtExecution() { + alignas(8) static std::uint32_t gate = 0; + static std::uint32_t done = 0; + alignas(16) static std::array record{0, 0, 0, 4}; + gate = 0; + done = 0; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x91, {low(record.data()), high(record.data()), 0}), + writeWord(done, 1)}); + submitWords(words); + record = {0x1000, 0, 6, 0}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran"); +} + void testSuspendPointWritesQueuedLabels() { alignas(8) static std::uint32_t gate = 0, marker = 0; gate = 0; @@ -1136,6 +1236,7 @@ int main(int argc, char** argv) { testIndexedDraw(); testAutoDraw(); testIndirectDraw(); + testIndexAttributesIndirect(); testMemory(); testCopies(); testMemoryCopyDecode(); @@ -1150,6 +1251,7 @@ int main(int argc, char** argv) { testUnwrittenUserData(); testDriverSubmission(); testRegisterListsReadAtSubmission(); + testIndexAttributesReadAtExecution(); testPredicatedSubmission(); testConditionalSubmission(); testSuspendPointWritesQueuedLabels(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d14eb624f2..5c1c6978c0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -95,6 +95,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [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. +- [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 - [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - only 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices: a `CB_COLOR_VIEW` slice range on an array target, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) From 972ce1224767c1770f8e17882cf972c5cc72a89f Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Tue, 13 Oct 2026 21:27:22 +0200 Subject: [PATCH 255/353] perf(agc): keep the cached address space for a read-only V# across two imports Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/GuestBufferMemory.hpp | 2 +- .../Graphics/src/GuestBufferMemory.cpp | 21 ++++++++-- .../libSceAgcDriver/tests/BdaResources.cpp | 42 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index e1b80ff503..5e5e7b8796 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -351,7 +351,7 @@ private: // The region of a descriptor-bound range (AddWritable/AddReadable), committed pages only. A // range inside a base region of the space adds nothing: the region serves it, as today's merge // of the two did. - void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept); + void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly); static bool servesView(const Region& region, std::uint64_t view); static bool sparseHeapCandidate(const Region& region); bool bindSparseMirror(Region& region, const GuestAllocations::Lease& lease); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 8f0be0a68e..1202de58ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2605,7 +2605,21 @@ const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address) return own; } -void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { +namespace { + +template +bool mirrorOverlaps(const Regions& base, std::uint64_t begin, std::uint64_t end) { + auto found = std::upper_bound(base.begin(), base.end(), begin, [](std::uint64_t value, const auto& region) { return value < region.begin; }); + if (found != base.begin()) --found; + for (; found != base.end() && found->begin < end; ++found) { + if (found->end > begin && found->mirror != nullptr) return true; + } + return false; +} + +} + +void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly) { validate(address, bytes); const auto begin = address & ~std::uint64_t{3}; const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3}; @@ -2618,6 +2632,7 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b } return; case BaseOverlap::Partial: + if (readOnly && !mirrorOverlaps(space->base, begin, end)) break; Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); dissolveSpace(false); break; @@ -2683,7 +2698,7 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: } void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { - addDescriptorRegion(address, bytes, atomic, swept); + addDescriptorRegion(address, bytes, atomic, swept, false); writes.emplace_back(address, address + bytes); } @@ -2691,7 +2706,7 @@ void GuestBufferMemory::AddReadable(std::uint64_t address, std::size_t bytes) { // Not in `writes`: no reference copy (copyRegion), no write-back, no pending-write note, no // direct-write mark, and UploadPrepare copies it without the device lock. A written descriptor // overlapping the range still covers it through its own Writes() entry. - addDescriptorRegion(address, bytes, false, false); + addDescriptorRegion(address, bytes, false, false, true); } void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 08fe07ffa1..692e214922 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -474,6 +474,47 @@ void importAtMapOverBudgetTests(const Context& context) { for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); } +void spaceReadAcrossImportsTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto boundary = first + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(boundary - 256, 512); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(boundary - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a read-only V# across two imports has no descriptor"); + const auto bytes = access.bytes(view.buffer); + Require(view.offset + adjustment + 512 <= bytes.size() && bytes[view.offset + adjustment] == std::byte{0x5a} && bytes[view.offset + adjustment + 256] == std::byte{0x6b}, "a read-only V# across two imports misses the bytes of either range"); + const auto ranges = leased.AddressRanges(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && boundary <= range.end; }) && std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= boundary && boundary + piece <= range.end; }), "an import is missing from the BDA table of a build with a read-only V# across it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a read-only V# across two imports dissolved the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1471,6 +1512,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { failedImportRemapTests(context, access); spaceImportRunTests(context); importWindowTests(context); + spaceReadAcrossImportsTests(context, access); importJoinTests(context); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); From ffc6c894e7a7e42ab983d5bdb187881a8b840c6f Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Mon, 12 Oct 2026 22:14:35 -0300 Subject: [PATCH 256/353] fix(agc): select coherent buffer heaps before allocation --- .../Graphics/include/Context.hpp | 27 +++++++++++ .../Graphics/src/Resources.cpp | 14 ++---- .../tests/DeviceMemoryFallback.cpp | 45 +++++++++++++++++++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c83858906b..d657c28ea8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -220,6 +220,33 @@ struct Context { throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } + std::uint32_t BufferMemoryType(std::uint32_t mask, VkMemoryPropertyFlags flags, VkDeviceSize bytes) const { + constexpr VkMemoryPropertyFlags coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + auto type = MemoryType(mask, flags); + if (flags == coherent) { + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((mask & (1u << i)) != 0 && (memory.memoryTypes[i].propertyFlags & (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) == (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) { + type = i; + break; + } + } + } + if ((flags & coherent) != coherent || memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + memoryProperties2(physical, &properties); + const auto heap = memory.memoryTypes[type].heapIndex; + const auto fits = [&](std::uint32_t candidate) { + return reported.heapBudget[candidate] != 0 && reported.heapUsage[candidate] < reported.heapBudget[candidate] && bytes <= reported.heapBudget[candidate] - reported.heapUsage[candidate]; + }; + if (reported.heapBudget[heap] == 0 || fits(heap)) return type; + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + const auto& candidate = memory.memoryTypes[i]; + if ((mask & (1u << i)) != 0 && (candidate.propertyFlags & flags) == flags && candidate.heapIndex != heap && fits(candidate.heapIndex)) return i; + } + return type; + } + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index c21f1d06ad..e675a2c212 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -81,17 +81,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag allocationBytes = allocation.allocationSize; // The CPU reads most of these buffers back (write-back, diffs), which is very slow from // write-combined memory, so the default host properties prefer cached host memory. - constexpr VkMemoryPropertyFlags hostDefault = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - if (properties == hostDefault) { - try { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); - } catch (const std::runtime_error&) { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault); - } - } else { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, properties); - } - if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize)) { + const bool useSlab = (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize); + allocation.memoryTypeIndex = context.BufferMemoryType(requirements.memoryTypeBits, properties, useSlab ? BufferPool::SlabBlockBytes(capacity) : allocation.allocationSize); + if (useSlab) { const auto slot = cache->TakeSlot(context, allocation.memoryTypeIndex, capacity, addressable); memory = slot.memory; offset = slot.offset; diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp index c139312bf4..2ed8ec1ba9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -22,11 +22,15 @@ constexpr std::uint64_t MiB = 1ull << 20u; std::uint64_t reportedBudget = 0; std::uint64_t reportedUsage = 0; +std::uint64_t reportedHostBudget = 0; +std::uint64_t reportedHostUsage = 0; VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); reported->heapBudget[0] = reportedBudget; reported->heapUsage[0] = reportedUsage; + reported->heapBudget[1] = reportedHostBudget; + reported->heapUsage[1] = reportedHostUsage; } Context mockContext(bool budgetExtension) { @@ -49,6 +53,46 @@ void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::u Expect(type == expected, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expected)); } +void BufferPlacementTests() { + constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + Context context = mockContext(true); + context.memory.memoryTypeCount = 4; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.memory.memoryTypes[2] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[3] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1}; + reportedBudget = 3000 * MiB; + reportedUsage = 2936 * MiB; + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + const auto choose = [&](const char* what, std::uint32_t mask, VkMemoryPropertyFlags flags, std::uint32_t expected) { + const auto type = context.BufferMemoryType(mask, flags, 64 * MiB); + Expect(type == expected, std::string(what) + ": chose " + std::to_string(type) + ", expected " + std::to_string(expected)); + }; + choose("coherent buffer fits exactly in preferred cached heap", 15, coherent, 0); + ++reportedUsage; + choose("coherent buffer moves before allocating from a full heap", 15, coherent, 1); + choose("buffer respects its allowed type mask", 5, coherent, 0); + choose("another type on the same full heap is not an alternative", 7, coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0); + choose("noncoherent alternative is not eligible", 9, coherent, 0); + choose("explicit device-local requirement is preserved", 15, coherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0); + choose("uncached-only coherent buffer preserves its type", 2, coherent, 1); + reportedHostUsage = reportedHostBudget - 64 * MiB + 1; + choose("alternative must have enough reported room", 15, coherent, 0); + reportedHostBudget = 0; + choose("unreported alternative budget is not assumed free", 15, coherent, 0); + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + reportedBudget = 0; + choose("unreported preferred budget retains cached preference", 15, coherent, 0); + reportedBudget = 3000 * MiB; + context.memoryProperties2 = nullptr; + choose("no budget query retains cached preference", 15, coherent, 0); + context.memoryProperties2 = mockMemoryProperties2; + context.physical = VK_NULL_HANDLE; + choose("no physical device retains cached preference", 15, coherent, 0); +} + } int main() { @@ -61,6 +105,7 @@ int main() { Choose("allocation exceeds the budget", true, 3000 * MiB, 2937 * MiB, both, HostType); Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); + BufferPlacementTests(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; From 53acbf0df63d3e05180a452731161f81a5b5c000 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Mon, 12 Oct 2026 22:39:49 -0300 Subject: [PATCH 257/353] test(agc): verify buffer budget placement before allocation --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 740248d8fc..db58878a43 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2692,6 +2692,8 @@ struct MockVulkan { VkDeviceSize allocatedBytes = 0; std::optional memoryLimit; std::uint64_t allocationAttempts = 0; + std::uint32_t bufferMemoryTypeBits = 1; + std::vector allocationTypes; std::vector layoutBindings; VkDescriptorSetLayoutCreateFlags layoutFlags = 0; std::vector layoutBindingFlags; @@ -2744,11 +2746,12 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - *requirements = {mock.bufferSizes.at(buffer), 1, 1}; + *requirements = {mock.bufferSizes.at(buffer), 1, mock.bufferMemoryTypeBits}; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { ++mock.allocationAttempts; + mock.allocationTypes.push_back(info->memoryTypeIndex); if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY; *memory = makeHandle(); mock.memories[*memory] = std::vector(info->allocationSize); @@ -3061,6 +3064,45 @@ void addressSpaceTeardownTests() { } using Role = ShaderRecompiler::DescriptorRole; + +void bufferBudgetPlacementTests() { + using namespace AgcDriver::Graphics; + for (const std::size_t bytes : {std::size_t{512}, std::size_t{3} << 20u}) { + for (const bool fail : {false, true}) { + mock = MockVulkan{}; + mock.bufferMemoryTypeBits = 3; + if (fail) mock.memoryLimit = 0; + { + auto context = mockContext(); + constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + context.memory.memoryTypeCount = 2; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4u << 20u, 0}; + context.memory.memoryHeaps[1] = {8u << 20u, 0}; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = [](VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = 1u << 20u; + reported->heapUsage[0] = reported->heapBudget[0] - 1024u; + reported->heapBudget[1] = 8u << 20u; + }; + const auto allocate = [&] { + Buffer buffer(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Require(buffer.Mapped() && buffer.Bytes().size() == bytes, "a budget-selected buffer lost its mapping or size"); + buffer.Bytes().front() = std::byte{0x5a}; + Require(mockBufferMemory(buffer.Handle()).front() == std::byte{0x5a}, "a budget-selected buffer bound the wrong allocation"); + }; + if (fail) expectFailure(allocate, "Vulkan result -2"); + else allocate(); + Require(mock.allocationTypes == std::vector{1} && mock.allocationAttempts == 1, "a coherent buffer did not select the other heap before its single allocation attempt"); + if (!fail) Require(mock.allocatedBytes == (bytes == 512 ? BufferPool::SlabBlockBytes(512) : bytes), "a coherent buffer budget check lost its full allocation extent"); + } + Require(mock.live == 0, "a budget-selected buffer leaked Vulkan objects"); + } + } +} using Kind = ShaderRecompiler::DescriptorKind; alignas(16) std::array guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444}; @@ -5780,6 +5822,10 @@ int main(int argc, char** argv) { std::cout << "address-space device teardown releases cached guest allocations\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--buffer-budget-placement") { + bufferBudgetPlacementTests(); + return 0; + } { AgcDriver::Graphics::PipelineCacheSynchronization synchronization; auto pipelineUse = synchronization.LockForPipelineUse(); @@ -5894,6 +5940,7 @@ int main(int argc, char** argv) { resourceTests(); descriptorSnapshotTests(); descriptorCacheTests(); + bufferBudgetPlacementTests(); misalignedShaderDataTests(); debugBranchTests(); textureCacheBudgetTests(); From 516b06f9450854459092cbf04f9960ddf8c97ca2 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:23:50 +0530 Subject: [PATCH 258/353] feat(libkernel): support sceKernelMprotect on foreign guest module images on Windows --- core/libs/prx/libkernel/CMakeLists.txt | 8 ++++++- .../libkernel/DirectMemory/DirectMemory.cpp | 22 ++++++++---------- core/libs/tests/GuestMemory.cpp | 23 +++++++++++++++++++ core/libs/tests/GuestMemoryModule.S | 3 +++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestMemoryModule.S diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a39d27df0e..b70bb730a5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1157,7 +1157,13 @@ if(BUILD_TESTING) target_include_directories(guest_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) configure_windows_unwind(guest_memory_tests) - add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) + if(WIN32) + add_library(guest_memory_module SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.S) + else() + add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) + endif() set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") add_dependencies(guest_memory_tests guest_memory_module) target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index a35b55b556..969da4ef4e 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -957,19 +957,17 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { #ifdef _WIN32 static bool IsGuestModuleImage(const void* base) { - std::wstring path(MAX_PATH, L'\0'); - for (;;) { - const auto length = GetModuleFileNameW(static_cast(const_cast(base)), path.data(), static_cast(path.size())); - if (length == 0) return false; - if (length < path.size()) { - path.resize(length); - break; - } - path.resize(path.size() * 2); + const auto address = reinterpret_cast(base); + const auto* dos = reinterpret_cast(address); + if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) return false; + const auto* nt = reinterpret_cast(address + static_cast(dos->e_lfanew)); + if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false; + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + for (unsigned i = 0; i < sectionCount; ++i) { + if (std::strncmp(reinterpret_cast(sections[i].Name), ".elf0", IMAGE_SIZEOF_SHORT_NAME) == 0) return true; } - constexpr std::wstring_view suffix = L".guest.prx"; - if (path.size() < suffix.size()) return false; - return std::equal(suffix.begin(), suffix.end(), path.end() - static_cast(suffix.size()), [](wchar_t expected, wchar_t actual) { return std::towlower(actual) == expected; }); + return false; } #endif diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 5c0916db99..44c533afe9 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -973,6 +973,29 @@ static void CheckGuestModuleImageProtection() { rejected = true; } Require(rejected); + + rejected = false; + try { + sceKernelMprotect(reinterpret_cast(&sceKernelMprotect), page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + + const auto prxPath = std::filesystem::path(path).parent_path() / "guest_memory_module_foreign.prx"; + std::filesystem::copy_file(path, prxPath, std::filesystem::copy_options::overwrite_existing); + void* foreignModule = dlopen_nid_postfix(prxPath.generic_string().c_str(), 2); + Require(foreignModule != nullptr); + auto* foreignTarget = GuestModulePage(foreignModule, page); + Require(!RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 1) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READONLY); + Require(RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 3) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READWRITE || ImageProtection(foreignTarget) == PAGE_WRITECOPY); + Require(dlclose_nid_postfix(foreignModule) == 0); + Require(!RegisteredGuestRange(foreignTarget, page)); + std::filesystem::remove(prxPath); } #endif diff --git a/core/libs/tests/GuestMemoryModule.S b/core/libs/tests/GuestMemoryModule.S new file mode 100644 index 0000000000..a16839b3a9 --- /dev/null +++ b/core/libs/tests/GuestMemoryModule.S @@ -0,0 +1,3 @@ +.section .elf0,"dr" +.p2align 4 +.long 0 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6b7e4134b1..0453bcd4fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -42,7 +42,7 @@ - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) -- [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits +- [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [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 From 866ed445b9d30f81156e51c323f4011cea4b454d Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:29:59 +0200 Subject: [PATCH 259/353] fix(agc): lower hull exec masks restored through phis to own lane bits A wave64 hull program narrows EXEC with s_and_saveexec_b64 and restores it later, so its exec words reach the lane-bit test as ANDs with the host ballot, as loop or join phis, and through separate ballots for the two halves. Tessellation lowering refused them as "uses a lane mask other than its own lane's bit". The own-lane bit of a mask pair is now rebuilt from the words' operations (ballots, bitwise ops, selects, phis, constants); both halves must have the same shape and ballot the same predicate. Dead hull cycles such as a saved-exec loop are removed before the remaining-ballot check, so a mask whose other lanes are read (s_bcnt1_i32_b64 exec) is still refused. The words are replaced through identities and block conditions are resolved, so a branch on a compare copied to VCC keeps its condition. Local and domain programs keep their exec words for the half-wave matcher. Block conditions fix and HullCompareBranch test from EagleBearerYS. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 20 ++ .../tests/execution/Tessellation.cpp | 50 +++- .../Optimization/src/TessellationLowering.cpp | 232 +++++++++++++++--- 3 files changed, 271 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6e412161ac..a52274cfa9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1391,6 +1391,25 @@ void verifyHalfWaveReduction() { }; require(reductions(vertex(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 vertex UMin scan, whose entry EXEC is every invocation, did not read lane 31 as a subgroup UMin on 32 lanes"); require(halves(vertex(scan(0x80000000u, 0x24000000u, 0x84000000u), 64u), spv::OpGroupNonUniformSMax, 0x80000000u), "half-wave reduction: the wave64 vertex SMax scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + const auto domain = [](std::vector code, std::uint32_t subgroupSize) { + *std::find(code.begin(), code.end(), 0xf800180fu) = 0xf80008cfu; + *std::find(code.begin(), code.end(), 0x7e160f00u) = 0x7e160f01u; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationEvaluation, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + request.useCache = false; + return Recompile(request).spirv; + }; + require(reductions(domain(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 domain UMin scan under s_orn2_saveexec_b64 vcc, exec did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(domain(scan(0u, 0x38000000u, 0x88000000u), 64u), spv::OpGroupNonUniformBitwiseOr, 0u), "half-wave reduction: the wave64 domain OR scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); const auto floatScan = [&scan](std::uint32_t identity, std::uint32_t vector, std::uint32_t join) { auto code = scan(identity, vector, 0u); code[23] = join; @@ -1432,6 +1451,7 @@ void verifyHalfWaveReduction() { auto overwritten = umin; overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); + require(reductions(domain(divergent, 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: domain keys masked inside a divergent branch did not read lane 31 as a subgroup UMin on 32 lanes"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 895a10e7f0..f264bdbecc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -42,6 +42,16 @@ alignas(256) constexpr std::array HullCode{ 0xbf810000, }; +alignas(256) constexpr std::array HullCompareBranch{ + 0xd4040016, 0x00020080, 0xbeea0416, 0xbf870001, 0xbf800000, 0xd7650000, 0x000100c1, 0xd7660000, + 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, + 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, 0xd5430016, + 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, 0xb06a0060, + 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array HullRestoredExecSaved{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -59,6 +69,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainCompareBranch{ + 0xf8000941, 0x00000000, 0xd4040014, 0x00020a80, 0xbeea0414, 0xbf870001, 0xbf800000, 0x34020e84, + 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, + 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, + 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, + 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array LocalIndexAsData{ 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, @@ -134,6 +152,27 @@ alignas(256) constexpr std::array HullAndSaveexec{ 0xbf810000, }; +alignas(256) constexpr std::array HullSaveexecLoop{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xbe980382, 0xbe9a0480, + 0x7d882881, 0xbe96246a, 0x889a7e1a, 0xbefe0416, 0x80988118, 0xbf078018, 0xbf85fff9, 0xe0781000, + 0x02020216, 0x7d882881, 0xbe94246a, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullLaneCount{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe98107e, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300218, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array DomainPatchRead{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, @@ -279,6 +318,12 @@ void ExpectRefusal(const ShaderRecompiler::SpirvTarget& target, ShaderStage stag Require(false, "a tessellation program was accepted although " + reason); } +void RequireCompareBranches(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + Require(!Compile(target, ShaderStage::TessellationEvaluation, DomainCompareBranch, cursor).spirv.empty(), "a domain program branching on a compare copied to VCC was not compiled"); + Require(!Compile(target, ShaderStage::TessellationControl, HullCompareBranch, cursor).spirv.empty(), "a hull program branching on a compare copied to VCC was not compiled"); +} + std::array PixelAt(std::uint32_t x, std::uint32_t y) { const auto offset = (static_cast(y) * Width + x) * 4u; return {std::to_integer(Pixels[offset]), std::to_integer(Pixels[offset + 1]), std::to_integer(Pixels[offset + 2]), std::to_integer(Pixels[offset + 3])}; @@ -538,7 +583,7 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); ExpectRefusal(target, ShaderStage::TessellationControl, HullHalfSlotIndex, "not a whole number of control points"); - ExpectRefusal(target, ShaderStage::TessellationControl, HullAndSaveexec, "lane mask"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullLaneCount, "lane mask"); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourRecord, "more than one patch-constant record", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainFixedPatchRead, "not indexed by its relative patch", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches); @@ -552,12 +597,15 @@ int main() { RequirePatches(*device, target, HullCode, "hull program"); RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + RequirePatches(*device, target, HullAndSaveexec, "hull program narrowing exec with s_and_saveexec_b64"); + RequirePatches(*device, target, HullSaveexecLoop, "hull program restoring exec through loop phis"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); RequireIntegerPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); + RequireCompareBranches(target); device.reset(); RequireRegisteredStages(); std::puts("Tessellation tests passed"); diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index b25f9fe61c..d75c10c039 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -69,39 +70,169 @@ bool IsLaneId(const IrValue* value) { return value != nullptr && value->Resolve()->Opcode() == IrOpcode::LaneId; } -IrValue* BallotSource(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - if (value->Opcode() != IrOpcode::CompositeExtractU32x4 || !IsImmediate(value->Argument(1), word)) return nullptr; - IrValue* ballot = value->Argument(0)->Resolve(); - return ballot->Opcode() == IrOpcode::Ballot ? ballot->Argument(0)->Resolve() : nullptr; -} - bool IsTrue(const IrValue* value) { return value != nullptr && value->HasImmediate() && value->Type() == IrType::Bool && value->ImmediateBool(); } -IrValue* HostLaneWord(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - while (value->Opcode() == IrOpcode::BitwiseAnd32 && IsTrue(BallotSource(value->Argument(1), word))) value = value->Argument(0)->Resolve(); - return value; +using LaneWords = std::pair; + +IrValue* BallotPredicate(IrValue* word, std::uint32_t half) { + IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && IsImmediate(word->Argument(1), half) ? ballot->Argument(0)->Resolve() : nullptr; } -IrValue* SavedLaneBit(IrValue* value) { - if (value->Opcode() != IrOpcode::INotEqual32) return nullptr; - IrValue* masked = VariableOperand(value, 0u); - if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return nullptr; - IrValue* shifted = VariableOperand(masked, 1u); - if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return nullptr; - IrValue* bit = shifted->Argument(1)->Resolve(); - if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return nullptr; - IrValue* word = shifted->Argument(0)->Resolve(); - if (word->Opcode() != IrOpcode::SelectU32) return BallotSource(HostLaneWord(word, 0u), 0u); - IrValue* low = word->Argument(0)->Resolve(); - if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return nullptr; - IrValue* source = BallotSource(HostLaneWord(word->Argument(1), 0u), 0u); - return source != nullptr && BallotSource(HostLaneWord(word->Argument(2), 1u), 1u) == source ? source : nullptr; +bool SamePredicate(const IrValue* lhs, const IrValue* rhs) { + if (lhs == nullptr || rhs == nullptr) return false; + if (lhs == rhs) return true; + return lhs->HasImmediate() && rhs->HasImmediate() && lhs->Type() == IrType::Bool && rhs->Type() == IrType::Bool && lhs->ImmediateBool() == rhs->ImmediateBool(); } +std::optional LaneBitWords(IrValue* value, std::uint32_t waveSize) { + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + IrValue* masked = VariableOperand(value, 0u); + if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + IrValue* shifted = VariableOperand(masked, 1u); + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return std::nullopt; + IrValue* bit = shifted->Argument(1)->Resolve(); + if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return std::nullopt; + IrValue* word = shifted->Argument(0)->Resolve(); + if (waveSize != 64u) return LaneWords{word, nullptr}; + if (word->Opcode() != IrOpcode::SelectU32) return LaneWords{word, word}; + IrValue* low = word->Argument(0)->Resolve(); + if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return LaneWords{word, word}; + return LaneWords{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; +} + +class LaneBits { +public: + explicit LaneBits(IrProgram& program) : program(program) {} + + IrValue* Of(const LaneWords& words) { + std::vector visited; + if (matches(words, visited)) return &build(words); + for (const auto& pair : visited) { + if (known.at(pair)) known.erase(pair); + } + return nullptr; + } + +private: + static LaneWords argument(const LaneWords& words, std::size_t index) { + return {words.first->Argument(index)->Resolve(), words.second == nullptr ? nullptr : words.second->Argument(index)->Resolve()}; + } + + bool matches(const LaneWords& words, std::vector& visited) { + if (const auto found = known.find(words); found != known.end()) return found->second; + known.emplace(words, true); + visited.push_back(words); + const bool result = check(words, visited); + if (!result) known[words] = false; + return result; + } + + bool check(const LaneWords& words, std::vector& visited) { + const auto [low, high] = words; + if (low->HasImmediate()) { + if (low->Type() != IrType::U32 || (low->ImmediateU32() != 0u && low->ImmediateU32() != 0xffffffffu)) return false; + return high == nullptr || (high->HasImmediate() && high->Type() == IrType::U32 && high->ImmediateU32() == low->ImmediateU32()); + } + if (high != nullptr && (high->HasImmediate() || high->Opcode() != low->Opcode())) return false; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + if (BallotPredicate(low, 0u) == nullptr) return false; + return high == nullptr || SamePredicate(BallotPredicate(low, 0u), BallotPredicate(high, 1u)); + } + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + return matches(argument(words, 0), visited) && matches(argument(words, 1), visited); + case IrOpcode::BitwiseNot32: + return matches(argument(words, 0), visited); + case IrOpcode::SelectU32: + if (high != nullptr && high->Argument(0)->Resolve() != low->Argument(0)->Resolve()) return false; + return matches(argument(words, 1), visited) && matches(argument(words, 2), visited); + case IrOpcode::Phi: + if (low->ArgumentCount() == 0 || low->ArgumentCount() != low->PhiBlockCount()) return false; + if (high != nullptr && (high->Parent() != low->Parent() || high->ArgumentCount() != low->ArgumentCount() || high->PhiBlockCount() != low->PhiBlockCount())) return false; + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) { + if (high != nullptr && high->PhiBlock(index) != low->PhiBlock(index)) return false; + if (!matches(argument(words, index), visited)) return false; + } + return true; + default: + return false; + } + } + + IrValue& boolean(bool value) { + IrValue& result = program.CreateValue(IrOpcode::Void, IrType::Bool); + result.SetImmediateBool(value); + return result; + } + + IrValue& emit(IrValue* position, IrOpcode opcode, std::initializer_list arguments) { + IrValue& value = program.CreateValue(opcode, IrType::Bool); + for (IrValue* argument : arguments) value.AddArgument(argument); + position->Parent()->InsertInstructionBefore(position, &value); + return value; + } + + IrValue& combine(IrValue* position, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + for (const auto& [fixed, other] : {std::pair{&lhs, &rhs}, std::pair{&rhs, &lhs}}) { + if (!fixed->HasImmediate()) continue; + if (opcode == IrOpcode::LogicalAnd) return fixed->ImmediateBool() ? *other : *fixed; + if (opcode == IrOpcode::LogicalOr) return fixed->ImmediateBool() ? *fixed : *other; + if (!fixed->ImmediateBool()) return *other; + } + return emit(position, opcode, {&lhs, &rhs}); + } + + IrValue& build(const LaneWords& words) { + IrValue* low = words.first; + if (low->HasImmediate()) return boolean(low->ImmediateU32() != 0u); + if (const auto found = built.find(words); found != built.end()) return *found->second; + IrValue* result = nullptr; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: + result = BallotPredicate(low, 0u); + break; + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: { + const auto opcode = low->Opcode() == IrOpcode::BitwiseAnd32 ? IrOpcode::LogicalAnd : low->Opcode() == IrOpcode::BitwiseOr32 ? IrOpcode::LogicalOr : IrOpcode::LogicalXor; + IrValue& lhs = build(argument(words, 0)); + IrValue& rhs = build(argument(words, 1)); + result = &combine(low, opcode, lhs, rhs); + break; + } + case IrOpcode::BitwiseNot32: { + IrValue& operand = build(argument(words, 0)); + result = operand.HasImmediate() ? &boolean(!operand.ImmediateBool()) : &emit(low, IrOpcode::LogicalNot, {&operand}); + break; + } + case IrOpcode::SelectU32: { + IrValue& chosen = build(argument(words, 1)); + IrValue& other = build(argument(words, 2)); + result = &emit(low, IrOpcode::SelectU1, {low->Argument(0)->Resolve(), &chosen, &other}); + break; + } + default: { + IrValue& phi = program.CreateValue(IrOpcode::Phi, IrType::Bool); + low->Parent()->InsertInstructionBefore(low, &phi); + built.emplace(words, &phi); + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) phi.AddPhiOperand(low->PhiBlock(index), &build(argument(words, index))); + return phi; + } + } + built.emplace(words, result); + return *result; + } + + IrProgram& program; + std::map known; + std::map built; +}; + bool Implies(IrValue* rawPredicate, IrValue* rawCondition) { IrValue* predicate = rawPredicate->Resolve(); IrValue* condition = rawCondition->Resolve(); @@ -420,17 +551,14 @@ public: : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)) {} void Run() { - for (IrBlock* block : program.BlockOrder()) { - for (IrValue* inst : block->Instructions()) { - if (IrValue* source = SavedLaneBit(inst)) inst->ReplaceAllUsesWith(source); - } - } + if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); for (const auto& access : accesses) rewrite(access); if (stage == IrShaderStage::TessellationEvaluation) removePrimitiveExports(); constexpr DeadCodeEliminator deadCodeEliminator; deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::TessellationControl) removeUnobservedCycles(); std::vector relative; for (IrBlock* block : program.BlockOrder()) { for (IrValue* inst : block->Instructions()) { @@ -453,6 +581,20 @@ public: } private: + void lowerOwnLaneBits() { + LaneBits laneBits(program); + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + const auto words = LaneBitWords(inst, program.WaveSize()); + if (!words) continue; + if (IrValue* bit = laneBits.Of(*words)) inst->ReplaceUsesWith(bit, true); + } + } + ResolveControlFlowIdentities(program); + constexpr DeadCodeEliminator identities; + identities.RemoveIdentities(program); + } + void collect() { const auto& memoryInfo = program.Resources().memoryInfo; for (IrBlock* block : program.BlockOrder()) { @@ -621,6 +763,36 @@ private: } } + void removeUnobservedCycles() { + std::unordered_set observed; + std::vector pending; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->MayHaveSideEffects() && observed.insert(inst).second) pending.push_back(inst); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (argument != nullptr && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (!observed.contains(inst)) unobserved.push_back(inst); + } + } + for (IrValue* inst : unobserved) { + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) inst->ReplaceArgument(index, nullptr); + } + for (IrValue* inst : unobserved) { + inst->Parent()->RemoveInstruction(inst); + inst->Invalidate(); + } + } + void requireFirstInvocation(const Access& access) { std::unordered_set path; if (!FirstInvocationOnly(access.predicate, evaluator, path)) Fail("per-patch tessellation output is not written by control point 0 alone", access.pc); From 65d56b314745ba6f10e9c0b682447affbb62ec77 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 18:23:54 +0000 Subject: [PATCH 260/353] fix(libSceNet): dissolve a datagram association on connect to AF_UNSPEC sceNetConnect() on a connected datagram socket rejected an address whose family is AF_UNSPEC (a null address) with EINVAL before touching the socket, so the socket stayed connected: sceNetGetpeername() still reported the old peer and datagrams from any other sender were dropped by the host. FreeBSD 11.4 disconnects a connected datagram socket first and only then looks at the address. sys/kern/uipc_socket.c, soconnectat(): * If protocol is connection-based, can only connect once. * Otherwise, if connected, try to disconnect first. This allows * user to disconnect by connecting to, e.g., a null address. */ if (so->so_state & (SS_ISCONNECTED|SS_ISCONNECTING) && ((so->so_proto->pr_flags & PR_CONNREQUIRED) || (error = sodisconnect(so)))) { udp_disconnect() (sys/netinet/udp_usrreq.c) clears the peer and keeps the local port. connect(2): "Datagram sockets may dissolve the association by connecting to an invalid address, such as a null address." libkernel's connect gets the same change in #4498. A connected datagram socket given an address whose family byte is 0 is now disconnected on the host first (AF_UNSPEC on Linux, a zeroed address of the socket's family on Windows), and the call then fails with EINVAL as before. Linux also drops an ephemeral local port on that disconnect, so the port is bound again on the wildcard address, which is what FreeBSD leaves. A host failure during the disconnect is returned as the call's error. Stream sockets keep the previous path. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 35 +++++++++++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 39 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 75 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 6038c9c9ca..592892bad2 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -763,6 +763,38 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { return descriptor; } +static std::uint16_t& native_port(sockaddr_storage& address) { + return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; +} + +static int disconnect_datagram(const Sock& socket) { + const auto value = socket.native->value; + sockaddr_storage peer{}; + NativeLength peer_length = sizeof(peer); + if (::getpeername(value, reinterpret_cast(&peer), &peer_length) != 0) return 0; + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + if (::getsockname(value, reinterpret_cast(&local), &local_length) != 0) return fail(native_error()); +#ifdef _WIN32 + sockaddr_storage unspecified{}; + unspecified.ss_family = local.ss_family; + if (::connect(value, reinterpret_cast(&unspecified), local_length) != 0) return fail(native_error()); +#else + sockaddr unspecified{}; + unspecified.sa_family = AF_UNSPEC; + if (::connect(value, &unspecified, sizeof(unspecified)) != 0) return fail(native_error()); +#endif + sockaddr_storage current{}; + NativeLength current_length = sizeof(current); + if (::getsockname(value, reinterpret_cast(¤t), ¤t_length) != 0) return fail(native_error()); + if (native_port(current) != 0) return 0; + sockaddr_storage wildcard{}; + wildcard.ss_family = local.ss_family; + native_port(wildcard) = native_port(local); + if (::bind(value, reinterpret_cast(&wildcard), local_length) != 0) return fail(native_error()); + return 0; +} + int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { Sock socket; { @@ -772,6 +804,9 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { it->second.reported_error = 0; socket = it->second; } + if (socket.type == NET_SOCK_DGRAM && addr && addrlen >= 2 && static_cast(addr)[1] == 0) { + if (const int error = disconnect_datagram(socket)) return error; + } sockaddr_storage native{}; NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1d856385e1..668799cac2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -664,6 +665,43 @@ static void CheckSocketError() { Require(sceNetSocketClose(refused) == 0); } +static void CheckDatagramDisconnect() { + const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); + const int other = sceNetSocket("disconnect-other", 2, 2, 17); + const int dissolved = sceNetSocket("disconnect", 2, 2, 17); + Require(peer >= 0 && other >= 0 && dissolved >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + std::array dissolvedAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + Require(sceNetBind_nid_postfix(dissolved, dissolvedAddress.data(), dissolvedAddress.size()) == 0); + std::uint32_t size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0); + size = dissolvedAddress.size(); + Require(sceNetGetsockname(dissolved, dissolvedAddress.data(), &size) == 0); + Require(sceNetConnect(dissolved, peerAddress.data(), peerAddress.size()) == 0); + std::array name{}; + size = name.size(); + Require(sceNetGetpeername(dissolved, name.data(), &size) == 0 && name[2] == peerAddress[2] && name[3] == peerAddress[3]); + const std::array unspecified{16, 0}; + Require(Failed(sceNetConnect(dissolved, unspecified.data(), unspecified.size()), 22)); + size = name.size(); + Require(Failed(sceNetGetpeername(dissolved, name.data(), &size), 57)); + std::array local{}; + size = local.size(); + Require(sceNetGetsockname(dissolved, local.data(), &size) == 0 && local[2] == dissolvedAddress[2] && local[3] == dissolvedAddress[3]); +#ifndef _WIN32 + Require(local[4] == 0 && local[5] == 0 && local[6] == 0 && local[7] == 0); +#endif + const int timeout = 2000000; + Require(sceNetSetsockopt(dissolved, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + const char unrelated[] = "unrelated sender"; + Require(sceNetSendto(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); + char buffer[32]{}; + Require(sceNetRecvfrom(dissolved, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(unrelated)); + Require(std::strcmp(buffer, unrelated) == 0); + for (const int descriptor : {peer, other, dissolved}) Require(sceNetSocketClose(descriptor) == 0); +} + int main() { const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); @@ -689,6 +727,7 @@ int main() { CheckUnboundSocketName(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); + CheckDatagramDisconnect(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index b38cd18e8b..d3471a625a 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -8,6 +8,7 @@ ### Functional +- [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported From fce953802a1576497feb7874df9c3db8a988e492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 13 Oct 2026 16:54:06 +0200 Subject: [PATCH 261/353] fix(libSceNet): fail the AF_UNSPEC connect with EAFNOSUPPORT as FreeBSD does Co-Authored-By: Claude --- core/libs/prx/libSceNet/Export.cpp | 1 + core/libs/tests/GuestSceNet.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 592892bad2..4a852b9e3d 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -806,6 +806,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { } if (socket.type == NET_SOCK_DGRAM && addr && addrlen >= 2 && static_cast(addr)[1] == 0) { if (const int error = disconnect_datagram(socket)) return error; + return fail(NET_EAFNOSUPPORT); } sockaddr_storage native{}; NativeLength native_length = 0; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 668799cac2..26899731f0 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -683,7 +683,7 @@ static void CheckDatagramDisconnect() { size = name.size(); Require(sceNetGetpeername(dissolved, name.data(), &size) == 0 && name[2] == peerAddress[2] && name[3] == peerAddress[3]); const std::array unspecified{16, 0}; - Require(Failed(sceNetConnect(dissolved, unspecified.data(), unspecified.size()), 22)); + Require(Failed(sceNetConnect(dissolved, unspecified.data(), unspecified.size()), 47)); size = name.size(); Require(Failed(sceNetGetpeername(dissolved, name.data(), &size), 57)); std::array local{}; From 2eda9f893c614bacd214de56f34cb3a45b73886b Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:01:41 +0200 Subject: [PATCH 262/353] feat(agc): rebuild the hull's relative patch index when it is used as data Hull programs whose relative patch index (v1 bits 7:0) reached anything but a tessellation ring or LDS address threw, because the lowering runs every Vulkan patch as relative patch 0. The quad terrain hull 0x80002fc700 in Ghost of Yotei is one, and so is the domain 0x8068fbd900 (v7). Such hulls and domains now take (PrimitiveID mod GE_CNTL.PRIM_GRP_SIZE) mod VGT_LS_HS_CONFIG.NUM_PATCHES, as if hull threadgroups took NUM_PATCHES patches at a time out of each primitive group; both stages see the same PrimitiveID for a patch. This holds only when every instance starts a new threadgroup, so it needs GE_CNTL.BREAK_WAVE_AT_EOI (bit 18); with the bit clear the instances share threadgroups and such programs still throw. PRIM_GRP_SIZE and BREAK_WAVE_AT_EOI join the tessellation configuration (draw keys, hull and domain cache keys, request version 24). A missing GE_CNTL throws, and so do local programs, whose relative vertex index depends on vertex reuse that Vulkan does not expose. --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 2 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 + .../libSceAgcDriver/Graphics/src/State.cpp | 3 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 12 +++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 28 ++++++++-- .../tests/execution/Tessellation.cpp | 56 +++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 2 + .../ControlFlow/src/RequestSerializer.cpp | 10 +++- .../Optimization/ShaderStageInputInfo.hpp | 2 + .../Optimization/src/TessellationLowering.cpp | 31 ++++++++-- core/shader/recompiler/Recompiler.hpp | 2 + .../src/ShaderInputInfoBuilder.cpp | 2 + docs/dev/debt/shader.md | 1 + 15 files changed, 142 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 069e72aeb1..a456ce969f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); } mix(graphics.rectList); mix(programs.size()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 8369224ac7..10297ffef8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -118,7 +118,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup; + return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance; }; if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false; if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adc88aee57..6733d42519 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -666,6 +666,8 @@ bool ValidationKey(const Context& context, std::span shade add(tessellation.partitioning); add(tessellation.outputTopology); add(tessellation.patchesPerGroup); + add(tessellation.primitiveGroupSize); + add(tessellation.breakAtInstance ? 1u : 0u); } } return keyed; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 2a7abf526c..e010e98c22 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -833,6 +833,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.partitioning); append(key, tessellation.outputTopology); append(key, tessellation.patchesPerGroup); + append(key, tessellation.primitiveGroupSize); + append(key, tessellation.breakAtInstance); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 2a7172224d..c7e4a0b17f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -646,7 +646,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); const auto config = read(queue.context, 0x2d6); const auto parameters = read(queue.context, 0x2db); - ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu}; + const auto primitiveGroup = find(queue.userConfig, 0x25b, RegisterBank::UserConfig); + ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu, primitiveGroup == queue.userConfig.end() ? 0u : primitiveGroup->second & 0x1ffu, primitiveGroup != queue.userConfig.end() && (primitiveGroup->second & 0x40000u) != 0u}; 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")); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c31f7f4000..7bd45dff22 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -748,6 +748,7 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x200d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "Patch topology and HS_EN disagree"); queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; queue.context[0x2d6] = 0xc355; queue.context[0x2db] = 0x40049; queue.context[0x286] = 0x42800000; @@ -765,7 +766,7 @@ void ShaderStageTests() { } Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; - Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently"); + Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "the passthrough triangle-patch configuration decoded differently"); for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); @@ -775,9 +776,16 @@ void ShaderStageTests() { queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; + queue.userConfig[0x25b] = 0x5fe3f; const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages; const auto& quads = quadStages.tessellation; - Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently"); + Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63 && quads->primitiveGroupSize == 63 && quads->breakAtInstance, "counter-clockwise quad patches with HOS levels [1, 9], 63 patches per group and BREAK_WAVE_AT_EOI decoded differently"); + queue.userConfig[0x25b] = 0x8040; + const auto shared = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(shared && shared->primitiveGroupSize == 64 && !shared->breakAtInstance, "GE_CNTL 0x8040 did not decode as 64 primitives per group with BREAK_WAVE_AT_EOI clear"); + queue.userConfig.erase(0x25b); + const auto missing = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(missing->primitiveGroupSize == 0 && !missing->breakAtInstance, "a draw without GE_CNTL decoded a primitive group size or BREAK_WAVE_AT_EOI"); queue.context[0x2db] = 0x40069; Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently"); for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a52274cfa9..3cab2f43d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1709,10 +1709,10 @@ void verifyMeshConfiguration() { RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; - hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}}; + hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u, 63u, true}, {}}; const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull)); const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr; - require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization"); + require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u && replayed->primitiveGroupSize == 63u && replayed->breakAtInstance, "the quad tessellation configuration, its patches per group, its primitive group size or BREAK_WAVE_AT_EOI were lost in serialization"); auto grouped = hull; grouped.graphics->tessellation->patchesPerGroup = 62u; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { @@ -1723,6 +1723,26 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(grouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index"); } + auto primitiveGrouped = hull; + primitiveGrouped.graphics->tessellation->primitiveGroupSize = 126u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + primitiveGrouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(primitiveGrouped, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); + } + auto shared = hull; + shared.graphics->tessellation->breakAtInstance = false; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + shared.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(shared, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + } const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -1864,14 +1884,14 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; minimal.context.pixel->layerExported = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23"); + require(requestPrefix(encoded, 8u) == "NVNQQRgAAAA=", "new requests did not use serialization version 24"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; for (std::size_t bytes = 0; bytes < 22u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class or layer export flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRkAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index f264bdbecc..e0e687fbc6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace { @@ -69,6 +70,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainRelativeData{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0x4a2a0e81, 0x7e2a0d15, 0x102a2aff, 0x3e000000, + 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, + 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, + 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainCompareBranch{ 0xf8000941, 0x00000000, 0xd4040014, 0x00020a80, 0xbeea0414, 0xbf870001, 0xbf800000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, @@ -104,6 +113,16 @@ alignas(256) constexpr std::array HullPatchConstant{ 0x0202070b, 0xe0701064, 0x0202060b, 0xbf810000, }; +alignas(256) constexpr std::array HullRelativeData{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0x4a182481, 0x7e180d0c, 0xe0701064, 0x02020c0b, 0xbf810000, +}; + alignas(256) constexpr std::array HullUnguardedFactors{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -251,6 +270,17 @@ alignas(256) constexpr std::array Indices{ Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), }; +alignas(256) constexpr std::array LatePatchIndices{ + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), +}; + alignas(256) constexpr std::array QuadIndices{ Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 2, 2), Corner(0, 0, 2), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 2, 2), Corner(1, 0, 2), @@ -497,6 +527,27 @@ void RequireIntegerPartitioning(AgcDriver::VulkanDevice& device, const ShaderRec Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "integer partitioning: the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); } +void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [hull, domain, name] : {std::tuple{std::span(HullRelativeData), std::span(DomainPatchRead), "hull"}, std::tuple{std::span(HullCode), std::span(DomainRelativeData), "domain"}}) { + for (const auto& [patches, blue] : {std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u, true}, Indices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 2u, 4u, true}, Indices}, 32}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 4u, true}, LatePatchIndices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 2u, true}, Indices}, 32}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, 32}, std::pair{2u, blue}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group did not take its blue channel from the " + name + " program's relative patch index, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); + } + } +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -589,6 +640,10 @@ int main() { ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainCulledPrimitive, "passthrough primitive"); ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -605,6 +660,7 @@ int main() { RequireIntegerPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); + RequireRelativePatches(*device, target); RequireCompareBranches(target); device.reset(); RequireRegisteredStages(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 6f51a855af..137ca7b8fd 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -108,6 +108,8 @@ private: for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); + append(key, tessellation->primitiveGroupSize); + append(key, tessellation->breakAtInstance); } template diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 217d54eee7..f7ecdc5598 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -523,6 +523,8 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.partitioning); writer.WriteU32(configuration.outputTopology); writer.WriteU32(configuration.patchesPerGroup); + writer.WriteU32(configuration.primitiveGroupSize); + writer.WriteBool(configuration.breakAtInstance); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -533,6 +535,10 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.partitioning = reader.ReadU32(); configuration.outputTopology = reader.ReadU32(); if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32(); + if (version >= 24u) { + configuration.primitiveGroupSize = reader.ReadU32(); + configuration.breakAtInstance = reader.ReadBool(); + } return configuration; } @@ -752,7 +758,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(23u); + writer.WriteU32(24u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -784,7 +790,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 23u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 24u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index f5029b374d..9c521c43ea 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -113,6 +113,8 @@ struct ShaderTessellationInputInfo { std::uint32_t partitioning = 0; std::uint32_t outputTopology = 0; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index d75c10c039..8d9e91c426 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -572,8 +572,9 @@ public: Fail("a tessellation ring base is used outside a lowered ring access", 0u); } } - requireRelativeIndependence(relative); - for (IrValue* base : relative) base->ReplaceAllUsesWith(&constant(0u)); + const bool data = usedAsData(relative); + if (data) requireGroupedPatches(); + for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; @@ -732,7 +733,7 @@ private: return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; } - void requireRelativeIndependence(const std::vector& bases) { + bool usedAsData(const std::vector& bases) { std::vector pending(bases.begin(), bases.end()); std::unordered_set visited(bases.begin(), bases.end()); while (!pending.empty()) { @@ -758,9 +759,31 @@ private: } if (dynamicInputAddress(user, value)) continue; const auto address = evaluator.Evaluate(value, nullptr); - if (!address || address->relative != 0) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (!address || address->relative != 0) return true; } } + return false; + } + + void requireGroupedPatches() const { + if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); + if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + } + + IrValue& groupedPatch(IrValue& base) { + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) value.AddArgument(argument); + base.Parent()->InsertInstructionBefore(&base, &value); + return value; + }; + const auto remainder = [&](IrValue& value, std::uint32_t divisor) -> IrValue& { + IrValue& count = constant(divisor); + return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); + }; + IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9a22de5259..d10b07f18b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -289,6 +289,8 @@ struct TessellationConfiguration { std::uint32_t partitioning; std::uint32_t outputTopology; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 4bce08497c..7fbb203fa3 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -189,6 +189,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.partitioning = tessellation->partitioning; target.outputTopology = tessellation->outputTopology; target.patchesPerGroup = tessellation->patchesPerGroup; + target.primitiveGroupSize = tessellation->primitiveGroupSize; + target.breakAtInstance = tessellation->breakAtInstance; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 03725ac06a..d4eafc72ff 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -89,6 +89,7 @@ - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). From c1e4a0ba8b8998fa6ed12d3536b23c570b70bde4 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:20:40 +0200 Subject: [PATCH 263/353] fix(shader): record BDA faults in hull programs instead of refusing them Every hull program with a BDA access was refused with "BDA fault termination requires a barrier-safe tessellation-control execution protocol". The refusal predates BdaInvocationsMayStop: a program with s_barrier no longer stops a faulting invocation; the fault is recorded, the read returns zero and every invocation reaches the barrier. In a hull, s_barrier is the only control barrier the backend emits after the guest code starts, so that rule already covers it, and a hull without one can stop an invocation without stranding a barrier. The tessellation test draws hulls that scale their levels by a BDA read, with and without s_barrier, and requires an unmapped read to report a tessellation-control fault (not on llvmpipe, which runs hull loads that the unmapped-address check skips). The contract test now accepts a hull with s_barrier. --- .../libSceAgcDriver/tests/BdaContracts.cpp | 2 +- .../tests/execution/Tessellation.cpp | 102 +++++++++++++++++- .../SpirvBackend/src/SpirvBdaRead.cpp | 1 - 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 3af9d345d9..cf54e315dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -41,7 +41,7 @@ void RunBdaContractTests() { ValidateBdaTarget(program, target); AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); program.Resources().stage = IrShaderStage::TessellationControl; - reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + ValidateBdaTarget(program, target); program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index e0e687fbc6..b3cf120de6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -1,14 +1,22 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include #include #include #include #include +#include #include #include #include @@ -24,7 +32,9 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Width = 256; constexpr std::uint32_t Height = 80; constexpr std::array Background{16, 24, 40, 255}; +constexpr std::size_t GuestBytes = 65536; alignas(256) std::array Pixels{}; +std::array HullUserData{0x1000u}; alignas(256) constexpr std::array LocalCode{ 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, @@ -192,6 +202,26 @@ alignas(256) constexpr std::array HullLaneCount{ 0x0202070b, 0xbf810000, }; +alignas(256) constexpr std::array HullBdaScale{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullBdaScaleWithoutBarrier{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, 0x7e320306, + 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, + 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array DomainPatchRead{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, @@ -304,11 +334,10 @@ struct Stages { }; ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles) { - static const std::array hullUserData{0x1000u}; static const std::array domainUserData{}; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const bool hull = stage == ShaderStage::TessellationControl; - const auto userData = hull ? std::span(hullUserData) : stage == ShaderStage::TessellationEvaluation ? std::span(domainUserData) : std::span(); + const auto userData = hull ? std::span(HullUserData) : stage == ShaderStage::TessellationEvaluation ? std::span(domainUserData) : std::span(); std::optional pixel; std::optional vertex; std::optional graphics; @@ -548,6 +577,74 @@ void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompi } } +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, 2 * GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestBytes, 2 * GuestBytes)); +#endif + Require(block != nullptr, "tessellation: cannot allocate the guest block"); + std::memset(block, 0, 2 * GuestBytes); + GuestAllocations::Mutation().Add(block, GuestBytes, 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; } + std::uint64_t Address() const { return reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +void RequireHullBdaReads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + GuestBlock guest; + const float scale = 1.0f; + std::memcpy(guest.Data(), &scale, sizeof(scale)); + const auto address = [](std::uint64_t value) { + HullUserData[6] = static_cast(value); + HullUserData[7] = static_cast(value >> 32u); + }; + const bool llvmpipe = device.DeviceName().starts_with("llvmpipe"); + for (const auto& [hull, program] : {std::pair{std::span(HullBdaScale), std::string("hull program scaling its levels by a BDA read after s_barrier")}, std::pair{std::span(HullBdaScaleWithoutBarrier), std::string("hull program scaling its levels by a BDA read without s_barrier")}}) { + address(guest.Address()); + RequirePatches(device, target, hull, program); + if (llvmpipe) { + std::puts(("skipped the unmapped read of the " + program + ": llvmpipe runs tessellation-control loads that the check of the unmapped address skips").c_str()); + continue; + } + address(guest.Address() + GuestBytes); + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor), + Compile(target, ShaderStage::TessellationControl, hull, cursor), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor), + Compile(target, ShaderStage::Fragment, PixelCode, cursor), + }; + std::string failure; + try { + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + } catch (const std::exception& error) { + failure = error.what(); + } + device.WaitIdle(); + Require(failure.find("BDA access failed") != std::string::npos && failure.find("stage=7 reason=1") != std::string::npos, program + ": the unmapped read was reported as \"" + failure + "\", not as a tessellation-control BDA fault"); + } + address(0u); +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -661,6 +758,7 @@ int main() { RequireQuads(*device, target); RequirePatchConstants(*device, target); RequireRelativePatches(*device, target); + RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); RequireRegisteredStages(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 663a001ba7..81a96b13ce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -359,7 +359,6 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe for (const auto extension : {"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}) { if (std::find(target.supportedExtensions.begin(), target.supportedExtensions.end(), extension) == target.supportedExtensions.end()) throw std::runtime_error(std::string("BDA requires unsupported extension ") + extension); } - if (program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe tessellation-control execution protocol"); } bool BdaInvocationsMayStop(const IrProgram& program) { From 46989a7d8a75f1597b62700dd859187d41552dc3 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 16:00:06 +1300 Subject: [PATCH 264/353] docs(libkernel): keep the memory protection bits entry --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0453bcd4fa..dc39a26fc2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -42,6 +42,7 @@ - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [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 From 472437a70345c80e2c3c078d4afa371d587f0c5a Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sun, 11 Oct 2026 22:09:51 +0000 Subject: [PATCH 265/353] fix(libkernel): report socket status from fstat on a guest socket fstat on a guest socket descriptor passed the guest number to the host, which has no such descriptor, and threw ("FillFileStat: fstat failed for fd 268435460"); sceKernelFstat returned EBADF. FreeBSD fills the stat buffer for a socket through soo_stat (sys/kern/sys_socket.c): bzero((caddr_t)ub, sizeof (*ub)); ub->st_mode = S_IFSOCK; ... if ((sb->sb_state & SBS_CANTRCVMORE) == 0 || sbavail(sb)) ub->st_mode |= S_IRUSR | S_IRGRP | S_IROTH; ub->st_size = sbavail(sb) - sb->sb_ctl; ... if ((sb->sb_state & SBS_CANTSENDMORE) == 0) ub->st_mode |= S_IWUSR | S_IWGRP | S_IWOTH; and pru_sense_null (sys/kern/uipc_socket.c) adds st_blksize = so->so_snd.sb_hiwat. fstat and sceKernelFstat now answer a guest socket from the socket table: S_IFSOCK, the read bits until the receive side is shut down and drained, the write bits until the send side is shut down, st_size from the bytes queued and st_blksize from the send buffer size, other fields zero. shutdown records which side it closed. A closed socket descriptor returns EBADF. The differences from the console (peer shutdown, the host's FIONREAD and SO_SNDBUF, socketpair identity) are listed in TechnicalDebt.md. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 10 ++++- .../Socket/include/SocketRuntime.hpp | 3 ++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 44 ++++++++++++++++++- core/libs/tests/GuestSocketPair.cpp | 25 +++++++++++ docs/dev/debt/libkernel.md | 3 +- 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d2bc6f450b..cdc7694ca0 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -721,7 +721,10 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (sb == nullptr) { APS5_INVALID_ARG_EX; } - if (GuestSockets::IsOpen(d)) throw std::runtime_error(std::string(__func__) + ": the status of a guest socket is not implemented"); + if (GuestSockets::IsOpen(d)) { + if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); + return 0; + } if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); return 0; } @@ -885,7 +888,10 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { int APS5_VABI sceKernelFstat(int d, FileStat* sb) { if (sb == nullptr) throw std::invalid_argument("sceKernelFstat: sb is null"); - if (GuestSockets::IsOpen(d)) throw std::runtime_error("sceKernelFstat: the status of a guest socket is not implemented"); + if (GuestSockets::IsOpen(d)) { + const int error = GuestSockets::Stat(d, sb); + return error == 0 ? 0 : SceErrorFromErrno(error); + } if (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); return 0; } diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 103f2d0684..61ee521433 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -3,6 +3,8 @@ #include #include #include "prx/libkernel/Socket/include/SocketPoll.hpp" + +struct FileStat; namespace GuestSockets { struct Iovec { void* base; @@ -16,6 +18,7 @@ bool IsOpen(int descriptor); int Family(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); +int Stat(int descriptor, FileStat* status); bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof); int Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds); struct Interest { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 76006ed159..7537e617df 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -15,6 +15,7 @@ #include #include #endif +#include "SceTypes.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -90,6 +91,8 @@ struct Socket { std::mutex modeMutex; bool nonblocking = false; GuestSockets::SocketOptionState options; + bool receiveShut = false; + bool sendShut = false; Socket(NativeSocket value, int family, int type) : value(value), family(family), type(type) {} ~Socket() { if (value == Invalid) return; @@ -596,6 +599,41 @@ int UnnamedAddress(void* address, std::uint32_t* length) { } } +int GuestSockets::Stat(int descriptor, FileStat* status) { + std::shared_ptr socket; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return 9; + socket = found->second; + } + bool receiveShut = false; + bool sendShut = false; + { + std::lock_guard lock(socket->modeMutex); + receiveShut = socket->receiveShut; + sendShut = socket->sendShut; + } + std::int64_t available = 0; +#ifdef _WIN32 + unsigned long count = 0; + if (ioctlsocket(socket->value, FIONREAD, &count) == 0) available = static_cast(count); +#else + int count = 0; + if (::ioctl(socket->value, FIONREAD, &count) == 0) available = count; +#endif + int sendBuffer = 0; + socklen_t length = sizeof(sendBuffer); + if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&sendBuffer), &length) != 0) sendBuffer = 0; + *status = FileStat{}; + status->st_mode = 0140000; + if (!receiveShut || available > 0) status->st_mode |= 0444; + if (!sendShut) status->st_mode |= 0222; + status->st_size = available; + status->st_blksize = sendBuffer > 0 ? static_cast(sendBuffer) : 0; + return 0; +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr socket; { @@ -897,7 +935,11 @@ int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); if (!socket) return Fail(9); if (how < 0 || how > 2) return Fail(22); - return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; + if (::shutdown(socket->value, how)) return Fail(NativeError()); + std::lock_guard lock(socket->modeMutex); + if (how != 1) socket->receiveShut = true; + if (how != 0) socket->sendShut = true; + return 0; } int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 3ab148efd5..15dabd1ef6 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -38,6 +39,8 @@ int APS5_VABI shutdown_nid_postfix(int, int); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); int* APS5_VABI __error_nid_postfix(); } @@ -131,6 +134,28 @@ int main() { Require(Exchange(pair[1], pair[0], "wake up")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0); + Require(status.st_mode == 0140666 && status.st_size == 0 && status.st_blksize != 0 && status.st_nlink == 0); + Require(send_nid_postfix(pair[0], "hello", 5, 0) == 5 && Readable(pair[1])); + Require(sceKernelFstat(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(shutdown_nid_postfix(pair[0], 1) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0 && status.st_mode == 0140444); +#ifndef _WIN32 + Require(shutdown_nid_postfix(pair[1], 0) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); +#else + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); + Require(shutdown_nid_postfix(pair[1], 0) == 0); +#endif + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140222 && status.st_size == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFstat(pair[1], &status) == static_cast(0x80020009u)); + Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 2); Require(send_nid_postfix(pair[0], "a", 1, 0) == 1); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6b7e4134b1..c6427539f6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -150,7 +150,8 @@ - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor -- [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, libkernel `fstat` on a socket throws, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` +- [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` +- [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level only, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_NOSIGPIPE`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them) and every other level (`IPPROTO_TCP`, `IPPROTO_IP`, `IPPROTO_IPV6`) fails with `ENOPROTOOPT`. None of this was measured on a console. From e25d561b7cd0626da87f7166c0093c1ec0056bd0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:57:30 -0700 Subject: [PATCH 266/353] fix(shader): take a hull's input stride from its patch term when it reads one control point derive() learned the LS-to-HS stride from the control-point term of hull LDS reads, or from the patch term of reads at a computed control-point index. A hull with one input control point reads it at constant offsets, so neither applies and the stride stays unknown ('the hull shader reads no LDS per control point, so the local stride is unknown'). Every hull read already has a patch term of exactly inputControlPoints * localStride, so take the stride from it for reads without a control-point term too. Resident Evil Village's quad patches have one input control point. --- .../tests/execution/Tessellation.cpp | 28 +++++++++++++++++++ .../Optimization/src/TessellationLowering.cpp | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index b3cf120de6..6edeb88bb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -63,6 +63,16 @@ alignas(256) constexpr std::array HullCompareBranch{ 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, }; +alignas(256) constexpr std::array HullSingleInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + alignas(256) constexpr std::array HullRestoredExecSaved{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -717,6 +727,23 @@ void RequireRegisteredStages() { AgcDriverShutdown_nid_postfix(); } +ShaderRecompiler::RecompileResult CompileHull(const ShaderRecompiler::SpirvTarget& target, std::span code, const ShaderRecompiler::TessellationConfiguration& configuration) { + static const std::array userData{0x1000u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::TessellationControl, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + ShaderRecompiler::BindingLayout{0, 0, 0, AgcDriver::Graphics::PipelinePushSlotBytes}, + ShaderRecompiler::GraphicsCompileContext{0u, {}, std::nullopt, configuration, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1}}, + }; + return ShaderRecompiler::Recompile(request); +} + +void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) { + Require(!CompileHull(target, HullSingleInput, {1u, 3u, 1u, 2u, 2u}).spirv.empty(), "a hull that reads only the one input control point of its patch was not compiled"); +} + } int main() { @@ -728,6 +755,7 @@ int main() { std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } + RequireSingleControlPointHulls(target); ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); ExpectRefusal(target, ShaderStage::TessellationControl, HullHalfSlotIndex, "not a whole number of control points"); diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 8d9e91c426..3834b28369 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -659,7 +659,7 @@ private: for (auto& access : accesses) { if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc); - if (access.kind == TessellationAttribute::ControlInput && access.address.dynamic && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); + if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::EvaluationInput) { const auto end = access.address.constant + 4 * static_cast(access.components); From 1243f067b1ead9d45b5ecda38e1f736eddca8916 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:58:04 -0700 Subject: [PATCH 267/353] fix(shader): lower hulls that write one output control point per patch A hull with one output control point writes its control point at the patch record offset alone, so the write has no control-point term and was taken for a patch constant with no control stride ('the hull shader writes off-chip memory outside its control points and patch constants'). Take the control stride from the patch term of such writes when the write fits in one record (its offset is at least 0 and its end within the patch term), the rule domain programs already apply to control-point reads; patch-constant records lie outside that, below the top of the off-chip block or above every control point. With one invocation per patch, per-patch outputs also need no control-point-0 guard. Resident Evil Village's quad patches have one output control point. --- .../libSceAgcDriver/tests/execution/Tessellation.cpp | 11 +++++++++++ .../Optimization/src/TessellationLowering.cpp | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 6edeb88bb1..80a851fb50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -73,6 +73,16 @@ alignas(256) constexpr std::array HullSingleInput{ 0xbf810000, }; +alignas(256) constexpr std::array HullSingleOutput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x7e2c0280, 0xd5430016, 0x045a24ff, 0x00000010, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0xbf800000, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + alignas(256) constexpr std::array HullRestoredExecSaved{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -742,6 +752,7 @@ ShaderRecompiler::RecompileResult CompileHull(const ShaderRecompiler::SpirvTarge void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) { Require(!CompileHull(target, HullSingleInput, {1u, 3u, 1u, 2u, 2u}).spirv.empty(), "a hull that reads only the one input control point of its patch was not compiled"); + Require(!CompileHull(target, HullSingleOutput, {1u, 1u, 1u, 2u, 2u}).spirv.empty(), "a hull with one output control point and unguarded patch outputs was not compiled"); } } diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 3834b28369..5ca2dc0cc6 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -656,11 +656,16 @@ private: void derive() { const auto inputs = static_cast(tessellation.inputControlPoints); const auto outputs = static_cast(tessellation.outputControlPoints); + const auto soleControlPoint = [&](const Access& access) { + const auto end = access.address.constant + 4 * static_cast(access.components); + return outputs == 1 && access.address.vertex == 0 && access.address.relative > 0 && access.address.constant >= 0 && end <= access.address.relative; + }; for (auto& access : accesses) { if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); + if (access.kind == TessellationAttribute::ControlOutput && soleControlPoint(access)) agree(controlStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::EvaluationInput) { const auto end = access.address.constant + 4 * static_cast(access.components); if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { @@ -692,7 +697,7 @@ private: if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % localStride != 0)) Fail("the hull shader reads LDS at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::ControlOutput: - if (address.vertex == 0) { + if (address.vertex == 0 && !soleControlPoint(access)) { if (controlStride == 0u || evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc); record(access, end); access.kind = TessellationAttribute::PatchOutput; @@ -817,6 +822,7 @@ private: } void requireFirstInvocation(const Access& access) { + if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; if (!FirstInvocationOnly(access.predicate, evaluator, path)) Fail("per-patch tessellation output is not written by control point 0 alone", access.pc); } From e5d65569e5985c41f97e6ec971e156dd551d90da Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 16:10:33 +1300 Subject: [PATCH 268/353] fix(agc): dissolve the cached address space when a written V# overlaps a read-only V# copied across imports A read-only V# that crosses two base regions is copied per build while the space stays. A written V# overlapping that copy would bind the base region's import while the read-only V# binds the copy of the same bytes. Dissolve the space in that case (either order of the two), as without this branch, so both share the one merged region. --- .../Graphics/include/GuestBufferMemory.hpp | 3 ++ .../Graphics/src/GuestBufferMemory.cpp | 13 +++++- .../libSceAgcDriver/tests/BdaResources.cpp | 45 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 5e5e7b8796..b3e34156e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -390,6 +390,9 @@ private: // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; + // Read-only descriptor ranges copied per build although they partially overlap the space's base + // regions (addDescriptorRegion); a written range overlapping one dissolves the space. + std::vector> readsOverBase; std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 1202de58ba..fa6fa693a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2632,7 +2632,12 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b } return; case BaseOverlap::Partial: - if (readOnly && !mirrorOverlaps(space->base, begin, end)) break; + // A read-only range keeps the space only while no written range of this build overlaps + // it: its copy and the base region's import would be two views of the same bytes. + if (readOnly && !mirrorOverlaps(space->base, begin, end) && !WritesOverlap(begin, static_cast(end - begin))) { + readsOverBase.emplace_back(begin, end); + break; + } Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); dissolveSpace(false); break; @@ -2698,6 +2703,12 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: } void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { + // A written range over a read-only range copied across base regions: the space dissolves, so the + // two share one merged region as they do without the space. + if (space != nullptr && std::any_of(readsOverBase.begin(), readsOverBase.end(), [&](const auto& range) { return address < range.second && range.first < address + bytes; })) { + Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); + dissolveSpace(false); + } addDescriptorRegion(address, bytes, atomic, swept, false); writes.emplace_back(address, address + bytes); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 692e214922..2a8e6ca460 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -515,6 +515,50 @@ void spaceReadAcrossImportsTests(const Context& context, const BdaTestAccess& ac ::operator delete(block, std::align_val_t{window}); } +void spaceReadAcrossImportsWrittenTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto boundary = reinterpret_cast(block) + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + // A written V# in the lower import whose tail a read-only V# across both imports also binds, + // added after it and before it: both must map the shared bytes through one buffer. + for (const bool writtenFirst : {false, true}) { + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + if (writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.AddReadable(boundary - 256, 512); + if (!writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.Upload(true); + std::uint32_t readAdjustment = 0; + std::uint32_t writeAdjustment = 0; + const auto read = leased.Descriptor(boundary - 256, 512, readAdjustment); + const auto written = leased.Descriptor(boundary - 384, 256, writeAdjustment); + Require(read.buffer != VK_NULL_HANDLE && read.buffer == written.buffer && written.offset + writeAdjustment + 128 == read.offset + readAdjustment, "a written V# inside a read-only V# across two imports maps other bytes than it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 0) Require(after.dissolvedOverlap == before.dissolvedOverlap + 1, "a written V# inside a read-only V# across two imports kept the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + } + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1513,6 +1557,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceImportRunTests(context); importWindowTests(context); spaceReadAcrossImportsTests(context, access); + spaceReadAcrossImportsWrittenTests(context); importJoinTests(context); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); From 7f2c2a51c3bc53abb79d936a195eac171f76b698 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 16:11:16 +1300 Subject: [PATCH 269/353] test(libkernel): give the module image protection test its own .elf0 module The shared guest_memory_module gained a .elf0 section, so on Windows its data was no longer counted against flexible memory and CheckGuestModuleUsesFlexibleMemory failed. Build the .elf0 variant as a separate guest_memory_elf0_module, keep the shared module as on main, and check that a .guest.prx module without .elf0 is rejected. --- core/libs/prx/libkernel/CMakeLists.txt | 16 +++++++++------- core/libs/tests/GuestMemory.cpp | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b70bb730a5..6d9e5fbdc6 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1157,16 +1157,18 @@ if(BUILD_TESTING) target_include_directories(guest_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) configure_windows_unwind(guest_memory_tests) - if(WIN32) - add_library(guest_memory_module SHARED - ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp - ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.S) - else() - add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) - endif() + add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") add_dependencies(guest_memory_tests guest_memory_module) target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") + if(WIN32) + add_library(guest_memory_elf0_module SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.S) + set_target_properties(guest_memory_elf0_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_elf0_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_ELF0_MODULE="$") + endif() add_test(NAME guest_memory COMMAND guest_memory_tests) if(WIN32) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 44c533afe9..348cb86d51 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -909,7 +909,7 @@ static bool RegisteredGuestRange(const void* pointer, std::size_t bytes) { static void CheckGuestModuleImageProtection() { constexpr std::size_t page = 0x4000; - const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + const auto path = std::filesystem::relative(GUEST_MEMORY_ELF0_MODULE).generic_string(); void* module = dlopen_nid_postfix(path.c_str(), 2); Require(module != nullptr); auto* target = GuestModulePage(module, page); @@ -996,6 +996,20 @@ static void CheckGuestModuleImageProtection() { Require(dlclose_nid_postfix(foreignModule) == 0); Require(!RegisteredGuestRange(foreignTarget, page)); std::filesystem::remove(prxPath); + + const auto plainPath = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + void* plainModule = dlopen_nid_postfix(plainPath.c_str(), 2); + Require(plainModule != nullptr); + auto* plainTarget = GuestModulePage(plainModule, page); + rejected = false; + try { + sceKernelMprotect(plainTarget, page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + Require(!RegisteredGuestRange(plainTarget, page)); + Require(dlclose_nid_postfix(plainModule) == 0); } #endif From f10ab0b6ab9dc9e3e897be9c458d828d91168c69 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:59:13 +0200 Subject: [PATCH 270/353] fix(relinker): validate complete relocation symbol entries --- .../src/pipeline/RelinkerPipeline.cpp | 23 ++++---- .../tests/test_executable_tls_relocations.py | 13 ++++- .../tests/test_jmprel_table_bounds.py | 52 +++++++++++++++---- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 6b9d54cf76..fde3d986d2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -225,6 +225,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf auto policy = std::dynamic_pointer_cast(validationPolicy); const std::vector& raw = elfReader->GetRawBytes(); + const std::optional dynSymTabSize = hasTag(DT_OS_SYMTABSZ) + ? std::optional(getTagValue(DT_OS_SYMTABSZ)) : std::nullopt; if (dynStrTabOffset > raw.size() || dynStrTabSize > raw.size() - dynStrTabOffset) throw RelinkerException("Dynamic string table is out of bounds", dynStrTabOffset); @@ -266,10 +268,17 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; - auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + auto symbolEntryPos = [&](const std::uint32_t symIdx) -> FileByteOffset { + const ByteCount off = static_cast(symIdx) * symEntSize; + const FileByteOffset symOff = dynSymTabOffset + off; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize + || (dynSymTabSize && (off > *dynSymTabSize || symEntSize > *dynSymTabSize - off))) throw RelinkerException("Symbol table entry out of bounds", symOff); + return symOff; + }; + + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint64_t value = 0; std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); return (raw[symOff + 4] & 0xf) == 3 && value == 0; @@ -283,9 +292,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) - throw RelinkerException("Symbol table entry out of bounds", symOff); + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint16_t section = 0; std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); if (section == SHN_UNDEF) @@ -325,9 +332,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (isTlsRelocation(relType) && (symIdx == 0 || definedTlsSymbol(symIdx, pos))) continue; - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) - throw RelinkerException("Symbol table entry out of bounds", symOff); + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index 0747677195..fd6ceff424 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -26,7 +26,7 @@ TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 -def image(relocations, tls=True): +def image(relocations, tls=True, symbol_bytes=None): """The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and an 8-byte TLS template, a symbol table and the given (offset, type, symbol, addend) relocations.""" data = fixture() @@ -43,6 +43,8 @@ def image(relocations, tls=True): for index, (offset, kind, symbol, addend) in enumerate(relocations): struct.pack_into(" Date: Wed, 14 Oct 2026 16:24:33 +1300 Subject: [PATCH 271/353] test(libkernel): check the libkernel.prx rejection on its own image &sceKernelMprotect in the test executable is its import thunk, so the call protected a page of the executable's own code read-only and the next call through it crashed. Use the libkernel.prx image instead. --- core/libs/tests/GuestMemory.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 348cb86d51..e2cd14c14f 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -976,7 +976,9 @@ static void CheckGuestModuleImageProtection() { rejected = false; try { - sceKernelMprotect(reinterpret_cast(&sceKernelMprotect), page, 1); + const auto libkernel = GetModuleHandleA("libkernel.prx"); + Require(libkernel != nullptr); + sceKernelMprotect(libkernel, page, 1); } catch (const std::invalid_argument&) { rejected = true; } From 9828710a38465475a71c6a133f4aba9673ce9fbd Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Sat, 10 Oct 2026 19:38:26 +0200 Subject: [PATCH 272/353] feat(videoout): implement sceVideoOutGetOutputStatusWithVrr Writes the 80-byte status described in #2746: the sceVideoOutGetOutputStatus fields followed by zeros, with the documented codes for a null status and an unknown handle. (cherry picked from commit e893efad2d0a7b90243d07bf6669cc9b5f540432) --- .../libs/prx/libSceVideoOut/Unimplemented.cpp | 5 --- core/libs/prx/libSceVideoOut/src/Output.cpp | 31 ++++++++++++++----- .../tests/GuestVideoOutResolutionStatus.cpp | 16 ++++++++++ docs/dev/debt/libSceVideoOut.md | 2 +- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 0c946149c5..ef58c55add 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -17,11 +17,6 @@ int APS5_VABI sceVideoOutClearFlipMaster() { return 0; } -int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) { - 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); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 2a333efcc3..35c3228916 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -16,6 +16,14 @@ static constexpr std::uint32_t VIDEO_OUT_OUTPUT_OPTIONS_FIRST_WORD = 0x00FF0000; static constexpr std::size_t VIDEO_OUT_OUTPUT_OPTIONS_FREE_WORD = 3; +static VideoOutOutputStatus outputStatus(const auto& cfg) { + VideoOutOutputStatus status{}; + status.resolution = (cfg.width >= 3840 || cfg.height >= 2160) ? 2u : 1u; + status.dynamicRange = 1; + status.refreshRate = (cfg.outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; + return status; +} + int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { switch (mode) { case VIDEO_OUT_OUTPUT_MODE_DEFAULT: @@ -159,13 +167,22 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu } std::unique_lock lock(cfg->mutex); cfg->Check(); - status->resolution = (cfg->width >= 3840 || cfg->height >= 2160) ? 2u : 1u; - status->dynamicRange = 1; - status->refreshRate = (cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; - status->flags = 0; - status->reserved[0] = 0; - status->reserved[1] = 0; - status->reserved[2] = 0; + *status = outputStatus(*cfg); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) try { + if (status == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::unique_lock lock(cfg->mutex); + cfg->Check(); + std::array bytes{}; + const auto output = outputStatus(*cfg); + std::memcpy(bytes.data(), &output, sizeof(output)); + std::memcpy(status, bytes.data(), bytes.size()); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/tests/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index 5294d7c29f..e401ad3023 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -1,6 +1,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/Shutdown.hpp" #include "tests/VideoOutTestEnvironment.hpp" +#include +#include #include #include #include @@ -10,6 +12,7 @@ extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param); int APS5_VABI sceVideoOutClose(int handle); int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status); +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status); int APS5_VABI sceVideoOutGetResolutionStatus(int handle, VideoOutResolutionStatus* status); } @@ -52,6 +55,18 @@ int main() { Require(sceVideoOutGetOutputStatus(handle, &output) == 0); Require(status.refreshRate == output.refreshRate); + std::array vrr; + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == 0); + Require(std::memcmp(vrr.data(), &output, sizeof(output)) == 0); + for (std::size_t offset = sizeof(output); offset < 80; ++offset) Require(vrr[offset] == std::byte{0}); + for (std::size_t offset = 80; offset < vrr.size(); ++offset) Require(vrr[offset] == std::byte{0xff}); + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, nullptr) == static_cast(0x80290002u)); + Require(sceVideoOutGetOutputStatusWithVrr(NEVER_OPENED_HANDLE, vrr.data()) == static_cast(0x8029000Bu)); + Require(sceVideoOutGetOutputStatusWithVrr(-1, vrr.data()) == static_cast(0x8029000Bu)); + for (const auto byte : vrr) Require(byte == std::byte{0xff}); + Require(Rejects(handle, nullptr)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); @@ -59,5 +74,6 @@ int main() { Require(sceVideoOutClose(handle) == 0); Require(Rejects(handle, &status)); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == static_cast(0x8029000Bu)); LibcRunShutdown_nid_postfix(); } diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 0d67a74ded..ce195aa306 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,7 +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 -- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. +- [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 - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented From 8702de1e6fb49b47bf18973e8588f8eeef33e8c0 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 17:11:46 +1300 Subject: [PATCH 273/353] ci(build): key the Mesa shader cache by the runner's CPU model too Since #5263 the Linux job restores a lavapipe shader cache keyed by the runner's /proc/cpuinfo flags. llvmpipe compiles shaders with LLVM's host CPU *name* (from vendor, family and model) plus the detected feature bits, and keys its cache only by the feature bits, so two runners with the same flags but different models share cached code. A hull shader JIT-compiled on a znver4/znver5 or Ice Lake class host uses GFNI (vgf2p8affineqb); loaded on a Zen 3 host with the same flags it raises SIGILL, which is what agc_driver_tessellation has been hitting on main and on dispatched builds since the first main build after #5234 saved that hull variant. Hash vendor_id, cpu family, model and flags into the key, so a runner only restores code compiled for its own CPU model, and print the model name so a future mismatch can be read off the log. --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4409a95534..e0b781895e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,7 +249,8 @@ jobs: - id: mesa run: | echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT" - echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" + grep -m1 '^model name' /proc/cpuinfo + echo "cpu=$(grep -E '^(vendor_id|cpu family|model|flags)[[:space:]]*:' /proc/cpuinfo | sort -u | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" - uses: actions/cache/restore@v6 with: path: .mesa-shader-cache From cf9dd4f181c8274858167f253f4f2a14a8a8038f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 04:18:43 +0000 Subject: [PATCH 274/353] fix(agc): refresh a block-copied texture the fast lookup would reuse fastTexture hands back the texture of a stage-A record when its memory is unchanged, and declines only depth surfaces and textures remade per use. The block copy of #4747 is a copy refreshed on reuse over a surface that is not a depth surface, so the fast path returned it without RefreshDepthCopy: after a compute pass stored new blocks into its storage image, a sample read the blocks of the previous copy. Decline copies refreshed on reuse in fastTexture as well, so the lookup takes cachedTexture, whose cache hit refreshes them. agc_driver_bc_block_copy failed on main at "sample of blocks stored again" (Linux, NVIDIA) and passes with this change. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index df5f105e54..f09d2cbcb6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3246,7 +3246,7 @@ bool ShaderResources::precollectImages() { } std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) { - if (record.texture == nullptr || record.texture->RefreshedPerUse()) return nullptr; + if (record.texture == nullptr || record.texture->RefreshedPerUse() || record.texture->IsDepthCopy()) return nullptr; struct Outcome { bool profile; std::chrono::steady_clock::time_point start; From 4589edf7f5565b1f22a8dfa10d85329b2cdc6864 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:39:11 -0300 Subject: [PATCH 275/353] perf(agc): present a display buffer from its slice of a pending array image Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/DisplayFormat.hpp | 2 ++ .../Execution/include/PresentationPass.hpp | 4 +-- .../Execution/include/PresentationScaler.hpp | 4 +-- .../Execution/include/VulkanDevice.hpp | 2 +- .../Execution/src/DisplayFormat.cpp | 7 +++++ .../Execution/src/PresentationPass.cpp | 8 ++--- .../Execution/src/PresentationScaler.cpp | 10 +++--- .../Execution/src/VulkanDevice.cpp | 31 +++++++++++-------- .../Graphics/include/GpuColorTransfer.hpp | 2 +- .../Graphics/src/GpuColorTransfer.cpp | 4 +-- 10 files changed, 44 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp index 899822c384..63e5d7749c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -4,6 +4,7 @@ #define VK_NO_PROTOTYPES #include #include +#include namespace AgcDriver { @@ -15,6 +16,7 @@ std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat); enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource); +std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp index 2f4a81a3f5..00e262b03f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -18,7 +18,7 @@ public: PresentationPass(const PresentationPass&) = delete; PresentationPass& operator=(const PresentationPass&) = delete; - void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat); + void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, std::uint32_t layer = 0); void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout); void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); void Release(std::size_t slot) noexcept; @@ -45,7 +45,7 @@ private: }; const Target& target(VkFormat format, VkImageLayout finalLayout); - VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage) const; + VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer = 0) const; VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; void ensureDumpTarget(Slot& slot, VkExtent2D extent); void releaseDumpTarget(Slot& slot) noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index 49209743ce..b0e88120aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -24,14 +24,14 @@ public: // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter); - void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height); + void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer = 0); // Copies the source image's texels (tightly packed rows of its format) into a host-readable buffer // and makes them visible to the host once the submission completes. void RecordReadback(VkCommandBuffer commands, VkBuffer destination); void RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); // The aspect-fitting blit of RecordBlit from any image: presenting a resident render target needs // no copy into the source image first. - static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer = 0); void RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); static std::vector LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight); std::uint32_t SourceWidth() const { return sourceWidth; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index bd48aed75c..5809ae0851 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -322,7 +322,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp index 7ca977ac58..d6447e9048 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -51,4 +51,11 @@ ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, } } +std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes) { + if (slices == 0 || displayBytes == 0 || imageBytes % slices != 0 || imageBytes / slices != displayBytes || displayAddress < imageAddress) return std::nullopt; + const auto offset = displayAddress - imageAddress; + if (offset % displayBytes != 0 || offset / displayBytes >= slices) return std::nullopt; + return static_cast(offset / displayBytes); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ace009d1fc..ee805692a0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -113,7 +113,7 @@ void PresentationPass::Release(std::size_t index) noexcept { slot.source = VK_NULL_HANDLE; } -VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImageUsageFlags usage) const { +VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer) const { VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usageInfo.usage = usage; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; @@ -122,7 +122,7 @@ VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImage 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}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, layer, 1}; VkImageView view = VK_NULL_HANDLE; Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView presentation pass"); return view; @@ -226,13 +226,13 @@ const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImag return targets.emplace(key, created).first->second; } -void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat) { +void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, std::uint32_t layer) { Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation pass source is unavailable"); Graphics::Require(!DisplayHalfFloat(pixelFormat), "presentation pass does not decode 16-bit float display buffers"); Release(index); auto& slot = slots[index]; - slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT); + slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT, layer); slot.width = width; slot.height = height; slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index 2c982db099..d7c88d7b7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -104,7 +104,7 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); } -void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height) { +void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer) { Graphics::Require(sourceImage != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "presentation image is unavailable"); Graphics::Require(width != 0 && height != 0, "presentation blit extent must be non-zero"); const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); @@ -118,9 +118,9 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; - blit.dstSubresource = blit.srcSubresource; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.dstOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; context.Function("vkCmdBlitImage")(commands, image, layout, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -144,11 +144,11 @@ void PresentationScaler::RecordBlit(VkCommandBuffer commands, VkImage destinatio RecordBlitFrom(context, commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sourceWidth, sourceHeight, VK_FILTER_LINEAR, destinationImage, destinationWidth, destinationHeight); } -void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { +void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer) { Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation blit source is unavailable"); const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[0] = {0, 0, 0}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 98209ca72c..58c7216df8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2492,10 +2492,12 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } } -ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter) { +ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { const auto& descriptor = image.Descriptor(); - if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageLayers() != 1 || image.ImageDepth() != 1) return ResidentPresent::None; - if (image.GuestBytes() != DisplayBufferSize(buffer)) return ResidentPresent::None; + if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1) return ResidentPresent::None; + const auto slice = ResidentPresentSlice(descriptor.baseAddress, image.GuestBytes(), image.ImageLayers(), buffer.address, DisplayBufferSize(buffer)); + if (!slice) return ResidentPresent::None; + layer = *slice; const auto format = Graphics::StorageFormatForGuest(context, descriptor.format); VkFormatProperties properties{}; context.formatProperties(context.physical, format, &properties); @@ -2504,12 +2506,13 @@ ResidentPresent ResidentPresentable(const Graphics::Context& context, const Grap return ResidentPresentPath(format, buffer.pixelFormat, blitSource); } -std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert) { +std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert, std::uint32_t& layer) { auto resident = Graphics::StorageTexture::FindPending(buffer.address, DisplayBufferSize(buffer)); pending = resident != nullptr; convert = false; + layer = 0; if (resident == nullptr) return resident; - const auto path = ResidentPresentable(context, *resident, buffer, filter); + const auto path = ResidentPresentable(context, *resident, buffer, filter, layer); if (path == ResidentPresent::None) resident.reset(); convert = path == ResidentPresent::Convert; return resident; @@ -2606,9 +2609,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::shared_ptr resident; VkFilter filter = VK_FILTER_LINEAR; bool convert = false; + std::uint32_t layer = 0; if (!NoResidentPresent()) { bool pending = false; - resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); + resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert, layer); if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) { resident.reset(); convert = false; @@ -2654,7 +2658,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; uniform = {{channel(2), channel(1), channel(0), channel(3)}}; } - if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr)) { + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; return false; @@ -2703,7 +2707,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -2808,7 +2812,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, residentStage, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); if (residentConvert && !drawn) { - state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat)); + state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat), residentLayer); state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); } } else { @@ -2819,11 +2823,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } if (drawn) { - state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat); + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, residentLayer); state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { state->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); + if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { @@ -2845,7 +2849,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } else if (direct) { if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); - slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); + slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height, residentLayer); slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); } else { state->scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); @@ -2940,7 +2944,8 @@ bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { VkFilter filter = VK_FILTER_LINEAR; bool pending = false; bool convert = false; - return PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr; + std::uint32_t layer = 0; + return PresentableResident(graphicsContext(), *buffer, filter, pending, convert, layer) == nullptr; } void VulkanDevice::FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index 246cc3ba94..95486e0e6c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -14,7 +14,7 @@ public: GpuColorTransfer& operator=(const GpuColorTransfer&) = delete; void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes = 4); void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false); - void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false); + void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false, std::uint32_t layer = 0); void Tile(VkCommandBuffer commands); void WriteBack(std::uint64_t address); VkBuffer LinearBuffer() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index b7d132af94..8bce1cc4f6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -149,13 +149,13 @@ void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swa barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); } -void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit, bool halfFloat) { +void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit, bool halfFloat, std::uint32_t layer) { Require(commands != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "color transfer image source is unavailable"); prepare(newWidth, newHeight, newMode, halfFloat ? 8u : 4u); Require(tiledDevice->Size() >= static_cast(width) * height * elementBytes, "color transfer buffer is smaller than the image"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); VkBufferImageCopy copy{}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; copy.imageExtent = {width, height, 1}; context.Function("vkCmdCopyImageToBuffer")(commands, image, layout, tiledDevice->Handle(), 1, ©); dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat); From a062eda972d3c50f7a35b45a623861694ce11bf0 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:50:07 -0300 Subject: [PATCH 276/353] test(agc): cover the slice decision and layered conversion of a resident presentation Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++ .../libSceAgcDriver/tests/ResidentPresent.cpp | 37 ++++++++++++++++--- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4b3f2ae5c1..2bc5f3a23d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1001,8 +1001,10 @@ if(BUILD_TESTING) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_resident_present COMMAND agc_driver_recorder_tests --resident-present-only) + set_tests_properties(agc_resident_present PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c3fa40e5b0..e0e834bfd6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6442,6 +6442,10 @@ int main(int argc, char** argv) { hostImportUnmapTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--resident-present-only") { + RunResidentPresentTests(device.GetContext()); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--parked-import-unmap-only") { gpu.unlock(); parkedImportUnmapTests(device); diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp index 9efc3e9265..718a636ed6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -20,6 +20,7 @@ namespace { using namespace AgcDriver::Graphics; using AgcDriver::DisplayBuffer; using AgcDriver::ResidentPresent; +using AgcDriver::ResidentPresentSlice; constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; @@ -53,7 +54,23 @@ std::uint32_t memoryType(const Context& context, std::uint32_t bits, VkMemoryPro throw std::runtime_error("no device-local memory type for the test image"); } -void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format) { +void sliceTests() { + constexpr std::uint64_t image = 0x208000000ull; + constexpr std::uint64_t slice = 0x1fe0000ull; + Require(ResidentPresentSlice(image, slice, 1, image, slice) == 0u, "a display buffer that is the whole image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image, slice) == 0u, "a display buffer at the start of a two-slice image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image + slice, slice) == 1u, "a display buffer one slice into a two-slice image is not its slice 1"); + Require(ResidentPresentSlice(image, 6 * slice, 6, image + 5 * slice, slice) == 5u, "a display buffer at the last of six slices is not slice 5"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 2 * slice, slice), "a display buffer past the image's last slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 0x10000, slice), "a display buffer between two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image - slice, slice), "a display buffer before the image has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, 2 * slice), "a display buffer the size of two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, slice / 2), "a display buffer of half a slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice + 1, 2, image, slice), "an image whose bytes do not divide into its slices has a slice"); + Require(!ResidentPresentSlice(image, 0, 0, image, slice) && !ResidentPresentSlice(image, slice, 1, image, 0), "an empty image or display buffer has a slice"); +} + +void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format, std::uint32_t layer = 0) { constexpr std::uint32_t width = 200; constexpr std::uint32_t height = 130; const auto texelBytes = AgcDriver::DisplayTexelBytes(pixelFormat); @@ -83,7 +100,7 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat imageInfo.format = format; imageInfo.extent = {width, height, 1}; imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; + imageInfo.arrayLayers = layer + 1u; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; @@ -132,12 +149,17 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransfer.srcQueueFamilyIndex = toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.image = image; - toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer + 1u}; barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toTransfer); VkBufferImageCopy region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; region.imageExtent = {width, height, 1}; context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + if (layer != 0) { + const VkClearColorValue other{}; + const VkImageSubresourceRange lower{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer}; + context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &other, 1, &lower); + } auto toGeneral = toTransfer; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; @@ -146,11 +168,11 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); transfer.Detile(commands, redLow, tenBit, halfFloat); readback(commands, fromGuest); - transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat); + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat, layer); readback(commands, fromImage); batch.SubmitAndWait(); } - const auto name = std::string(halfFloat ? "16-bit float " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from VkFormat " + std::to_string(static_cast(format)); + const auto name = std::string(halfFloat ? "16-bit float " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from layer " + std::to_string(layer) + " of VkFormat " + std::to_string(static_cast(format)); Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "the GPU detile differs from the CPU decode: " + name); Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "the resident image presents other pixels than guest memory: " + name); } @@ -159,11 +181,14 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat void RunResidentPresentTests(const Context& context) { decisionTests(); + sliceTests(); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32); conversionTest(context, Bgra8, VK_FORMAT_R8G8B8A8_UNORM); conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM); conversionTest(context, Rgba16Float, VK_FORMAT_R16G16B16A16_SFLOAT); + conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM, 1); + conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 2); std::cout << "resident present format and conversion tests passed\n"; } From ccd8591fb0a879d00c12e3c726756e340b4a8670 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:01:10 -0300 Subject: [PATCH 277/353] perf(agc): keep only the slice a draw rendered pending in an array target Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/include/Texture.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 17 +-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 15 +++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 100 ++++++++++++++++++ 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2bc5f3a23d..881ba4a34e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1003,8 +1003,10 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_resident_present COMMAND agc_driver_recorder_tests --resident-present-only) set_tests_properties(agc_resident_present PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_slice_dirty COMMAND agc_driver_recorder_tests --slice-dirty-only) + set_tests_properties(agc_slice_dirty PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 8b1adc9bc0..3be057ee70 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -182,6 +182,7 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); + void MarkSliceDirty(std::uint32_t slice); void Flush(); // Stores every pending image overlapping the range, except `except`; returns whether any was. // Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adc88aee57..f4050f2fbf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1482,7 +1482,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1517,8 +1517,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); } - for (const auto& resident : residents) { - if (resident != nullptr) resident->MarkDirty(); + for (std::size_t index = 0; index < residents.size(); ++index) { + if (residents[index] == nullptr) continue; + if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); + else residents[index]->MarkDirty(); } } @@ -1533,6 +1535,7 @@ struct RecordedDraw { std::shared_ptr framebuffer; std::span targetViews; std::vector> targets; + std::vector targetSlices; std::vector> proxies; const IndirectRecord* indirect = nullptr; std::span moved; @@ -1783,7 +1786,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); timer.phase(PhaseRecord); - keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); + keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); timer.phase(PhaseKeep); if (record.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2133,6 +2136,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.framebuffer = framebuffer; record.targetViews = targetViews; record.targets = owners; + for (const auto& binding : targets) record.targetSlices.push_back(binding.color.arraySlice); for (const auto& binding : targets) { if (binding.proxied) record.proxies.push_back(binding.resident); } @@ -2333,7 +2337,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector slices; for (const auto& binding : targets) slices.push_back(binding.color.arraySlice); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2378,7 +2382,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (skipTargetWrite) break; if (binding.resident != nullptr) { // The results stay on the GPU until something reads the target's memory. - binding.resident->MarkDirty(); + binding.resident->MarkSliceDirty(binding.color.arraySlice); continue; } if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); @@ -2517,6 +2521,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con record.framebuffer = std::move(framebuffer); record.targetViews = recipe.targetViews; record.targets = std::move(targets); + for (const auto& color : state.colors) record.targetSlices.push_back(color.arraySlice); record.pushBytes = &recipe.pushBytes; record.pushStages = recipe.pushStages; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 703a0e6017..2e769c4165 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2985,6 +2985,21 @@ void StorageTexture::MarkDirty() { markLayersPending(0, trackedLayers); } +void StorageTexture::MarkSliceDirty(std::uint32_t slice) { + if (geometry.thick || geometry.imageLayers <= 1 || slice >= geometry.imageLayers || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) { + MarkDirty(); + return; + } + const auto begin = geometry.GuestLayerOffset(slice); + const auto first = static_cast(begin / trackedLayerBytes); + const auto last = static_cast(std::min((begin + geometry.layerBytes + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + if (first >= last) { + MarkDirty(); + return; + } + markLayersPending(first, last - first); +} + bool StorageTexture::anyLayerPending() const { return std::any_of(layerPending.begin(), layerPending.end(), [](bool pending) { return pending; }); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index e0e834bfd6..6eddef89c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5577,6 +5577,101 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { expectRed(program.Red(flatTexture.View(), flatTexture.Layout(), 0.0f), 0x20 / 255.0f, "a 2D texture over the surface does not read its first layer"); } +void sliceDirtyTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t side = 128; + constexpr std::size_t sliceBytes = 65536; + constexpr std::size_t bytes = 4 * sliceBytes; + constexpr std::uint8_t untouched = 0x11; + for (const auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kR64KBX}) { + const std::string what = tileMode == TextureTileMode::kLinear ? "a linear two-slice image" : "a tiled two-slice image"; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: slice dirty marking not tested\n"; + return; + } + auto* memory = static_cast(block); + std::memset(memory, untouched, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, block, address, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(address, bytes); + TextureDetiler detiler(base); + auto context = base; + context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.depthOrLastArray = 1; + resource.mipCount = 1; + resource.tileMode = tileMode; + resource.dimension = TextureDimension::k2DArray; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 2 && geometry.layerBytes == sliceBytes && geometry.guestBytes == 2 * sliceBytes, what + " does not hold two 64 KiB slices"); + const auto stored = [&](std::uint32_t slice, std::array color) { + std::vector read(sliceBytes); + AgcDriver::GuestMemory::Read(address + geometry.GuestLayerOffset(slice), read); + for (std::size_t at = 0; at < read.size(); ++at) { + if (std::to_integer(read[at]) != color[at % 4]) return false; + } + return true; + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + const VkClearColorValue green{{0.0f, 1.0f, 0.0f, 1.0f}}; + const VkImageSubresourceRange first{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkImageSubresourceRange second{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 1, 1}; + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &first); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &green, 1, &second); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkSliceDirty(0); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}), what + ": the slice a draw wrote was not stored"); + Require(stored(1, {untouched, untouched, untouched, untouched}), what + ": a slice no draw wrote was stored over guest memory"); + static_cast(recorder.Commands()); + recorder.Keep(image); + image->MarkSliceDirty(1); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}) && stored(1, {0, 255, 0, 255}), what + ": the second slice was not stored once a draw wrote it"); + } + recorder.Sync(); + } + std::cout << "slice dirty marking: ok\n"; +} + void atomicViewTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); TextureDetiler detiler(context); @@ -6442,6 +6537,10 @@ int main(int argc, char** argv) { hostImportUnmapTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--slice-dirty-only") { + sliceDirtyTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--resident-present-only") { RunResidentPresentTests(device.GetContext()); return 0; @@ -6521,6 +6620,7 @@ int main(int argc, char** argv) { drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); + sliceDirtyTests(device, recorder); storeRunTests(device, recorder); queuedWriteGroupTests(); remappedImportTests(device); From ed95e6618e824696c96180a7eaf5866dd947d964 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:23:20 -0300 Subject: [PATCH 278/353] perf(agc): present a display buffer that is a later slice of a pending array image Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/include/Texture.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 12 ++++++++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 9 +++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 58c7216df8..45ccc24d26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2507,7 +2507,7 @@ ResidentPresent ResidentPresentable(const Graphics::Context& context, const Grap } std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert, std::uint32_t& layer) { - auto resident = Graphics::StorageTexture::FindPending(buffer.address, DisplayBufferSize(buffer)); + auto resident = Graphics::StorageTexture::FindPendingSlice(buffer.address, DisplayBufferSize(buffer)); pending = resident != nullptr; convert = false; layer = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 3be057ee70..be7a10c418 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -268,6 +268,7 @@ public: // contains a descriptor of its first mips), if any; the caller decides whether the geometry fits // (Texture::CanCopyFrom, ResidentPresentable). static std::shared_ptr FindPending(std::uint64_t address, std::uint64_t bytes); + static std::shared_ptr FindPendingSlice(std::uint64_t address, std::uint64_t bytes); // How a buffer fill of [address, address + bytes) (Driver.cpp's fill HLE) meets the storage // images alive: none of them; a whole surface (Exact) or one array layer of a surface (Layer), // `image` and `layer` naming it, whatever other images lie over the range (`others`, of which diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2e769c4165..348af5dedb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3256,6 +3256,18 @@ std::shared_ptr StorageTexture::FindPending(std::uint64_t addres return containing != nullptr ? containing->weak_from_this().lock() : nullptr; } +std::shared_ptr StorageTexture::FindPendingSlice(std::uint64_t address, std::uint64_t bytes) { + if (auto whole = FindPending(address, bytes)) return whole; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + const auto base = texture->descriptor.baseAddress; + if (texture->geometry.thick || texture->geometry.imageLayers <= 1 || address <= base || address - base >= texture->guestBytes || bytes > texture->guestBytes - (address - base)) continue; + return texture->weak_from_this().lock(); + } + return nullptr; +} + bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 6eddef89c6..c8110c7612 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -5652,6 +5653,14 @@ void sliceDirtyTests(const Device& device, Recorder& recorder) { context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &green, 1, &second); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); image->MarkSliceDirty(0); + for (std::uint32_t slice = 0; slice < 2; ++slice) { + const auto display = address + geometry.GuestLayerOffset(slice); + const auto found = StorageTexture::FindPendingSlice(display, sliceBytes); + Require(found == image, what + ": a display buffer that is slice " + std::to_string(slice) + " does not find the pending image"); + Require(AgcDriver::ResidentPresentSlice(found->Descriptor().baseAddress, found->GuestBytes(), found->ImageLayers(), display, sliceBytes) == slice, what + ": a display buffer that is slice " + std::to_string(slice) + " is not presented from that layer"); + } + Require(StorageTexture::FindPending(address + geometry.GuestLayerOffset(1), sliceBytes) == nullptr, what + ": the lookup by base address finds a display buffer in slice 1"); + Require(StorageTexture::FindPendingSlice(address + 2 * sliceBytes, sliceBytes) == nullptr && StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes + 1) == nullptr, what + ": a range past the image's last slice finds the pending image"); image->WriteBack(); recorder.Submit(); device.WaitQueue(); From 50621572287db377cbf93b5a8be6de337c44d043 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:32:46 -0300 Subject: [PATCH 279/353] perf(agc): refuse a display buffer that is a slice of two pending array images Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 14 +++++++++++--- .../prx/libSceAgcDriver/tests/Recorder.cpp | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 348af5dedb..bda9722ffe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3260,12 +3260,20 @@ std::shared_ptr StorageTexture::FindPendingSlice(std::uint64_t a if (auto whole = FindPending(address, bytes)) return whole; auto& pending = Pending(); std::lock_guard lock(pending.mutex); + StorageTexture* found = nullptr; for (auto* texture : pending.textures) { const auto base = texture->descriptor.baseAddress; - if (texture->geometry.thick || texture->geometry.imageLayers <= 1 || address <= base || address - base >= texture->guestBytes || bytes > texture->guestBytes - (address - base)) continue; - return texture->weak_from_this().lock(); + const auto layers = texture->geometry.imageLayers; + if (texture->geometry.thick || layers <= 1 || bytes == 0 || address <= base || texture->guestBytes % layers != 0 || texture->guestBytes / layers != bytes) continue; + if ((address - base) % bytes != 0 || (address - base) / bytes >= layers) continue; + if (found != nullptr) { + char message[256]; + std::snprintf(message, sizeof(message), "AGC graphics: 0x%llx+0x%llx is a slice of the pending array images 0x%llx and 0x%llx: which one holds its results is not modeled", static_cast(address), static_cast(bytes), static_cast(found->descriptor.baseAddress), static_cast(base)); + throw std::runtime_error(message); + } + found = texture; } - return nullptr; + return found != nullptr ? found->weak_from_this().lock() : nullptr; } bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c8110c7612..0564f2db2c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5675,6 +5675,24 @@ void sliceDirtyTests(const Device& device, Recorder& recorder) { device.WaitQueue(); recorder.Sync(); Require(stored(0, {255, 0, 0, 255}) && stored(1, {0, 255, 0, 255}), what + ": the second slice was not stored once a draw wrote it"); + auto other = StorageTexture::Create(context, detiler, resource, 0); + static_cast(recorder.Commands()); + recorder.Keep(image); + recorder.Keep(other); + image->MarkSliceDirty(1); + other->MarkSliceDirty(1); + std::string ambiguous; + try { + static_cast(StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes)); + } catch (const std::runtime_error& error) { + ambiguous = error.what(); + } + Require(ambiguous.find("is a slice of the pending array images") != std::string::npos, what + ": a slice of two pending array images did not throw: " + ambiguous); + image->WriteBack(); + other->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); } recorder.Sync(); } From 7ce7338242d65059fb0724532bf89680ef70f849 Mon Sep 17 00:00:00 2001 From: poonasor Date: Mon, 12 Oct 2026 23:20:09 -0400 Subject: [PATCH 280/353] perf(shader): read adjacent BDA dword loads of one block through one span read A scalar load of several dwords (s_load_dwordx8) becomes one LoadAddressU32 per dword, and each took its own span read: a probe of the page table and a fallback that looks every byte up. Loads in one block with equal address operands and offsets within 16 bytes of the first now share its span read. Instructions that write memory end a group; the address operands are compared by content, since each load carries its own GetAddressResource and SelectU32 values. An NHL 27 (PPSA34063) main-menu pixel shader goes from 136 span reads to 34. AMD's Windows compiler inlines the read helpers at every call, and did not finish that shader before. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 22 +++++- .../include/SpirvBackend/SpirvBda.hpp | 2 +- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvBdaRead.cpp | 4 +- .../SpirvMemory/SpirvMemoryInstructions.cpp | 79 +++++++++++++++++++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6e412161ac..7c34857f9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3008,10 +3008,10 @@ void verifyBdaReadFallbackFunctions() { 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 loads, bool barrier, bool coherent) { + const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent, std::uint32_t stride) { std::vector code{0x7e020200u, 0x7e040201u}; for (std::uint32_t load = 0; load < loads; ++load) { - code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (4u * load + 4u)); + code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (stride * load + 4u)); code.push_back(((3u + load) << 24u) | 0x007d0001u); } code.push_back(0xbf8c3f70u); @@ -3037,8 +3037,9 @@ void verifyBdaReadFallbackFunctions() { }; for (const bool barrier : {false, true}) { for (const bool coherent : {false, true}) { - const auto one = compile(1u, barrier, coherent); - const auto five = compile(5u, barrier, coherent); + const auto one = compile(1u, barrier, coherent, 32u); + const auto five = compile(5u, barrier, coherent, 32u); + const auto adjacent = compile(5u, barrier, coherent, 4u); std::map names; std::map definitions; const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); @@ -3090,6 +3091,19 @@ void verifyBdaReadFallbackFunctions() { require(mainProbes == 0u, "BDA read functions: a read site probes or reads bytes outside its span read function"); require(spanCalls == 5u, "BDA read functions: a read site does not call its span read function once"); require(five.size() - one.size() < 4u * 150u, "BDA read functions: a read site takes 150 SPIR-V words or more"); + std::size_t adjacentSpanCalls = 0; + names.clear(); + function.clear(); + for (std::size_t cursor = 5; cursor < adjacent.size();) { + const auto length = adjacent[cursor] >> 16u; + require(length != 0 && length <= adjacent.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); + const auto op = adjacent[cursor] & 0xffffu; + if (op == spv::OpName) names[adjacent[cursor + 1]] = reinterpret_cast(&adjacent[cursor + 2]); + if (op == spv::OpFunction) function = names[adjacent[cursor + 2]]; + if (op == spv::OpFunctionCall && function == "main" && names[adjacent[cursor + 3]] == span) ++adjacentSpanCalls; + cursor += length; + } + require(adjacentSpanCalls == 2u, "BDA read functions: five adjacent dwords of one address do not share two span reads"); } } } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 0b308f556c..2ded1d0e7b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -34,7 +34,7 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); +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); 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 f5d6a95d9e..afd26653ac 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -128,6 +128,7 @@ struct SpirvEmitterState { std::uint32_t dot2F32F16Function = 0; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; + std::unordered_map> bdaGroupedLoads; 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/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 663a001ba7..cf69ca5111 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -587,12 +587,12 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std // unaligned mirror or a wrapping span falls back to the byte path. That path repeats, per extracted // dword in order, the address, overflow check and byte lookups of a separate dword load, so every // fault is recorded exactly as one; the probe records none, and a span it accepts cannot fault. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword) { +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword, std::uint32_t usedDwords) { auto& state = ctx.state; if (dwords == 0u || dwords > 4u) ctx.Fail(inst, "unsupported BDA read width"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); const auto instruction = BdaInstructionPc(state, inst); - const auto used = everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); + const auto used = usedDwords != 0u ? usedDwords : everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); const auto isUsed = [&](std::uint32_t dword) { return (used & (1u << dword)) != 0u; }; std::array values{}; std::uint32_t first = dwords; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 64ae0cd782..2c1599802f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -202,7 +202,86 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +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; + if (left->HasImmediate() || right->HasImmediate()) return left->HasImmediate() && right->HasImmediate() && left->ArgumentCount() == 0u && left->ImmediateU32() == right->ImmediateU32(); + if (left->Opcode() != IrOpcode::GetAddressResource && left->Opcode() != IrOpcode::SelectU32) return false; + for (std::size_t index = 0; index < left->ArgumentCount(); ++index) { + const IrValue* first = left->Argument(index); + const IrValue* second = right->Argument(index); + if (first == nullptr || second == nullptr || !SameAddressValue(first->Resolve(), second->Resolve(), depth + 1u)) return false; + } + return true; +} + +bool WritesMemory(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + const auto shared = SharedAccessOf(opcode); + const auto address = AddressOpcodeInfoOf(opcode).access; + const auto image = ImageOpcodeInfoOf(opcode).access; + return buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || (shared != SharedAccess::None && shared != SharedAccess::Read) || address == AddressAccess::Write || address == AddressAccess::Atomic || + 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> members; + const IrBlock* block = inst.Parent(); + if (block == nullptr || ctx.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; + std::uint32_t taken = 1u; + for (++next; next != instructions.end(); ++next) { + const IrValue* other = *next; + if (other->Opcode() != inst.Opcode()) { + if (WritesMemory(other->Opcode())) break; + continue; + } + const auto& otherMemory = ctx.Memory(*other); + 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) { + const IrValue* left = inst.Argument(index); + const IrValue* right = other->Argument(index); + if (left == nullptr || right == nullptr || !SameAddressValue(left->Resolve(), right->Resolve())) sameArguments = false; + } + const auto delta = otherMemory.offset - mem.offset; + if (!sameArguments || delta == 0u || delta > 12u || (delta & 3u) != 0u || (taken & (1u << (delta / 4u))) != 0u) continue; + taken |= 1u << (delta / 4u); + members.emplace_back(other, delta / 4u); + } + return members; +} + std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + auto& state = ctx.state; + if (bits == 32u) { + if (const auto grouped = state.bdaGroupedLoads.find(&inst); grouped != state.bdaGroupedLoads.end()) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); + return value; + } + const auto members = AdjacentBdaLoads(ctx, inst, mem); + if (!members.empty()) { + std::uint32_t used = 1u; + for (const auto& member : members) used |= 1u << member.second; + const auto vector = TypeU32Vector(state, 4); + const auto zero = ConstantU32(state, 0u); + 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 read = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); + return read; + }); + for (const auto& member : members) state.bdaGroupedLoads[member.first] = {all, member.second}; + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, all, 0u); + return value; + } + } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); }); From bf6b6c0a17c912361c0e6e26d493f92f5dd14a9a Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 17:39:48 +1300 Subject: [PATCH 281/353] docs(relinker): describe fixed-base default for Windows executables --- docs/dev/debt/relinker.md | 2 +- docs/user/USAGE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index a74958d0ed..cbd42c4989 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,7 +4,7 @@ - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). -- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) so they can load at the fixed low image base inside the console 40-bit address window; Windows 10 PRAGMATA evidence is recorded in #3043, but this behavior has not been verified on console hardware or other Windows versions. +- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - `--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. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4e75ad5559..0a520c0235 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -77,7 +77,7 @@ The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, ## Runtime layout -For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires a fixed address. Without it, Windows placement and relocation behavior are unchanged. This option does not change Linux or macOS output. +For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change Linux or macOS output. Paths are relative to the output executable: From 3ddfc2395db5dbcfe724c78ed10325f94f173d46 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 17:41:55 +1300 Subject: [PATCH 282/353] test(agc): present slice 1 of an array image through the presentation pass --- .../tests/PresentationPass.cpp | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp index afa7afa3a3..94194bd4b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -115,14 +115,14 @@ private: class Image { public: - Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) { + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0, std::uint32_t layers = 1) : 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.arrayLayers = layers; info.samples = VK_SAMPLE_COUNT_1_BIT; info.tiling = VK_IMAGE_TILING_OPTIMAL; info.usage = usage; @@ -154,7 +154,7 @@ private: 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) { +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, std::uint32_t layers = 1) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.srcAccessMask = sourceAccess; barrier.dstAccessMask = destinationAccess; @@ -162,7 +162,7 @@ void transition(const Context& context, VkCommandBuffer commands, VkImage image, 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}; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers}; context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); } @@ -207,9 +207,9 @@ struct Source { std::vector decoded; }; -Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height) { +Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height, std::uint32_t salt = 0) { Source source{pixelFormat, storage, width, height, std::vector(static_cast(width) * height * 4), {}}; - std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u); + std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u) ^ salt; for (auto& byte : source.words) { seed = seed * 1664525u + 1013904223u; byte = static_cast(seed >> 24u); @@ -225,22 +225,29 @@ Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t wid return source; } -std::unique_ptr residentImage(const Context& context, const Source& source) { - auto image = std::make_unique(context, source.storage, VkExtent2D{source.width, source.height}, 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, source.words.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(upload.Bytes().data(), source.words.data(), source.words.size()); +std::unique_ptr residentImage(const Context& context, const std::vector& slices) { + const auto& source = *slices.front(); + const auto layers = static_cast(slices.size()); + auto image = std::make_unique(context, source.storage, VkExtent2D{source.width, source.height}, 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, layers); + Buffer upload(context, source.words.size() * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::vector regions(layers); + for (std::uint32_t layer = 0; layer < layers; ++layer) { + std::memcpy(upload.Bytes().data() + source.words.size() * layer, slices[layer]->words.data(), source.words.size()); + regions[layer].bufferOffset = source.words.size() * layer; + regions[layer].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; + regions[layer].imageExtent = {source.width, source.height, 1}; + } CommandBatch batch(context); const auto commands = batch.Handle(); - transition(context, commands, image->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 region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - region.imageExtent = {source.width, source.height, 1}; - context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT); + transition(context, commands, image->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, layers); + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layers, regions.data()); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT, layers); batch.SubmitAndWait(); return image; } +std::unique_ptr residentImage(const Context& context, const Source& source) { return residentImage(context, std::vector{&source}); } + std::vector convertPathPresent(const Context& context, const Source& source, VkExtent2D extent) { Buffer upload(context, source.decoded.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); std::memcpy(upload.Bytes().data(), source.decoded.data(), source.decoded.size()); @@ -265,11 +272,11 @@ std::vector convertPathPresent(const Context& context, const Source& return bytesOf(readback, bytes); } -std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format) { +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0) { Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); const auto bytes = static_cast(extent.width) * extent.height * 4; Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat); + pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat, layer); { CommandBatch batch(context); const auto commands = batch.Handle(); @@ -281,11 +288,11 @@ std::vector passPresent(const Context& context, PresentationPass& pas return bytesOf(readback, bytes); } -std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride) { +std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride, std::uint32_t layer = 0) { const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); const auto bytes = static_cast(extent.width) * extent.height * 4; Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat); + pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat, layer); { CommandBatch batch(context); pass.RecordDump(batch.Handle(), 1, stride, readback.Handle()); @@ -322,14 +329,14 @@ void passMatchesConvertPath(const Context& context, PresentationPass& pass, cons } } -void passDumpMatchesSubsample(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { +void passDumpMatchesSubsample(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t layer = 0) { for (const std::uint32_t stride : {1u, 3u, 4u}) { const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); std::vector expected(static_cast(extent.width) * extent.height * 4); for (std::uint32_t y = 0; y < extent.height; ++y) { for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); } - Require(passDump(context, pass, source, resident, stride) == expected, "the presentation dump differs from the subsampled guest-path frame: " + describe(source, extent) + " stride " + std::to_string(stride)); + Require(passDump(context, pass, source, resident, stride, layer) == expected, "the presentation dump differs from the subsampled guest-path frame: " + describe(source, extent) + " stride " + std::to_string(stride)); } } @@ -408,6 +415,21 @@ void blitLetterboxMatchesClear(const Context& context) { } } +void passPresentsSlice(const Context& context, PresentationPass& pass) { + const auto first = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto second = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130, 0x5a5a5a5au); + Require(first.decoded != second.decoded, "the two slices of the array test image hold the same texels"); + const auto resident = residentImage(context, std::vector{&first, &second}); + const std::array slices{&first, &second}; + for (std::uint32_t layer = 0; layer < slices.size(); ++layer) { + const auto& source = *slices[layer]; + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}}) { + Require(passPresent(context, pass, source, *resident, extent, VK_FORMAT_B8G8R8A8_UNORM, layer) == convertPathPresent(context, source, extent), "the presentation pass of slice " + std::to_string(layer) + " of an array image differs from the convert path: " + describe(source, extent)); + } + passDumpMatchesSubsample(context, pass, source, *resident, layer); + } +} + void passRefusesHalfFloat(PresentationPass& pass, const Image& resident) { bool refused = false; try { @@ -435,6 +457,7 @@ void residentPresentTests(const Context& context) { if (AgcDriver::DisplayTenBit(pixelFormat)) passKeepsTenBits(context, pass, source, *resident); if (pixelFormat == Bgra8) passRefusesHalfFloat(pass, *resident); } + passPresentsSlice(context, pass); blitLetterboxMatchesClear(context); } From e56a7c23512a5c292797815efa08d00b3c3005c1 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 17:40:41 +1300 Subject: [PATCH 283/353] fix(agc): save the guest bytes of pending storage results when their range leaves the write watch On Windows, singly mapped direct memory is private since #5596, so its host import is a direct import that leaves the write watch instead of a shared alias that stays watched. A storage image whose results were pending when its memory was imported had no saved guest bytes, and storing them threw. GuestMemory now calls a hook before Unwatch and before an ImportWatched that does not stay watched. Every image with results pending over the range saves the guest bytes of its pending units that have none, then collects and records the pieces the keep rule leaves to the CPU for the writes the watch saw. The next store and Refresh leave those pieces to the CPU and compare the rest with the saved bytes. A map-time import made without the GPU lock is deferred to the locked retry when pending results overlap the range. --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 16 ++++ .../Graphics/include/Texture.hpp | 5 ++ .../Graphics/src/GuestBufferMemory.cpp | 6 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 75 ++++++++++++++++++- .../tests/execution/StoreOverUntracked.cpp | 45 ++++++----- docs/dev/debt/libSceAgcDriver.md | 2 +- 7 files changed, 123 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 6e174a34ca..050ea60447 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -61,6 +61,7 @@ void WriteChanged(std::uint64_t address, std::span current, std bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); +void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); bool Watched(std::uint64_t address, std::size_t bytes); std::uint64_t CollectWrites(std::uint64_t address, std::size_t bytes); bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 3143d0b3e6..d7fca6508f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1173,6 +1173,12 @@ std::uint64_t CollectEpoch() { namespace { +std::atomic unwatchHook{nullptr}; + +void beforeUnwatch(std::uint64_t address, std::size_t bytes) { + if (const auto hook = unwatchHook.load(std::memory_order_acquire)) hook(address, bytes); +} + void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) { if (!tracker.watched) return; #ifdef _WIN32 @@ -1184,8 +1190,13 @@ void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t byt } +void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) { + unwatchHook.store(hook, std::memory_order_release); +} + void Unwatch(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return; + beforeUnwatch(address, bytes); auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1194,6 +1205,11 @@ void Unwatch(std::uint64_t address, std::size_t bytes) { bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); + if (!staysWatched) { + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + beforeUnwatch(first, static_cast(((address + bytes + page - 1) & ~(page - 1)) - first)); + } auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 8b1adc9bc0..21653fcd41 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -192,6 +192,7 @@ public: // overlapping pending image, as before). static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); + static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); @@ -362,6 +363,9 @@ private: void writeBack(std::uint64_t address, std::size_t bytes); void writeBackLayers(const std::vector& layers); bool unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const; + void saveBeforeUnwatch(); + void forgetKeptAtUnwatch(std::uint32_t unit); + bool keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const; bool guestBytesSettled() const; // Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB // aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one @@ -560,6 +564,7 @@ private: std::uint64_t version = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; + std::map keptAtUnwatch; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 8f0be0a68e..35c41043be 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2237,7 +2237,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& const auto range = mapped.lock(); if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { - if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false; + if (!GuestMemory::GpuMutex().HeldByThisThread()) { + const auto run = importRun(current, range->address); + const std::array, 1> span{{{run.base, run.base + run.bytes}}}; + if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; + } if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast(range->address), range->bytes, error.what()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 703a0e6017..3f89f804c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1053,6 +1053,11 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { std::shared_ptr StorageTexture::Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; + static const bool unwatchHooked = [] { + GuestMemory::SetUnwatchHook(&StorageTexture::SaveBeforeUnwatch); + return true; + }(); + static_cast(unwatchHooked); auto texture = std::shared_ptr(new StorageTexture(context, detiler, descriptor, mipLevel)); uploadReason = "first"; texture->upload(); @@ -1781,7 +1786,13 @@ bool StorageTexture::Refresh() { cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); - if (compared) cpuBlocks = stampedBlocks; + if (compared) { + for (const auto& [from, unitGeneration] : keptAtUnwatch) { + const auto unit = static_cast((from - descriptor.baseAddress) / trackedLayerBytes); + if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten; + } + cpuBlocks = stampedBlocks; + } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; keys = ProvedKeys(); @@ -3160,6 +3171,63 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons return true; } +void StorageTexture::SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes) { + std::vector> overlapping; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + const auto take = [&](StorageTexture* texture) { + if (!texture->overlaps(address, bytes)) return; + if (auto alive = texture->weak_from_this().lock()) overlapping.push_back(std::move(alive)); + }; + for (auto* texture : pending.textures) take(texture); + for (auto* texture : pending.flushing) take(texture); + } + if (overlapping.empty()) return; + Require(GuestMemory::GpuMutex().HeldByThisThread(), "AGC graphics: memory under pending storage image results leaves the write watch on a thread without the GPU lock"); + for (const auto& texture : overlapping) texture->saveBeforeUnwatch(); +} + +void StorageTexture::forgetKeptAtUnwatch(std::uint32_t unit) { + const auto begin = layerBegin(unit); + keptAtUnwatch.erase(keptAtUnwatch.lower_bound(begin), keptAtUnwatch.lower_bound(begin + layerBytes(unit))); +} + +bool StorageTexture::keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const { + const auto found = keptAtUnwatch.find(from); + return found != keptAtUnwatch.end() && layerOriginal[unit] && found->second == layerGeneration[unit]; +} + +void StorageTexture::saveBeforeUnwatch() { + std::vector units(trackedLayers, false); + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit] && layerGeneration[unit] != 0; + const auto runs = unitRuns(units); + if (runs.empty()) return; + original.resize(static_cast(guestBytes)); + comparedGuestBytes = {}; + for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); + constexpr std::uint64_t block = 65536; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (!units[unit]) continue; + const auto begin = layerBegin(unit); + const auto end = begin + layerBytes(unit); + const auto spanBegin = begin & ~(block - 1); + const auto spanBlocks = static_cast((end - spanBegin + block - 1) / block); + const std::vector generations(spanBlocks, layerGeneration[unit]); + std::vector changedBlocks(spanBlocks); + if (!GuestMemory::ChangedBlocks(begin, static_cast(end - begin), generations, changedBlocks)) continue; + forgetKeptAtUnwatch(unit); + for (auto at = spanBegin; at < end; at += block) { + const auto from = std::max(at, begin); + const auto to = std::min(at + block, end); + const bool edge = from != at || to != at + block; + if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); + } + layerOriginal[unit] = true; + } +} + bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); @@ -4279,7 +4347,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto to = std::min(at + block, end); if (from >= to) continue; const bool edge = from != at || to != at + block; - if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) { + if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; @@ -4289,6 +4357,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { else keep.emplace_back(from, to); } } + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer]) forgetKeptAtUnwatch(layer); + } // Debug aid: APS5_TRACE_FLUSH also names the blocks a write-back leaves to the CPU. static const bool traceKept = std::getenv("APS5_TRACE_FLUSH") != nullptr; if (traceKept && skippedAny) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3caabc9f92..f7f8b8105c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -205,7 +205,7 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch }; +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch }; struct Case { const char* name; @@ -218,11 +218,11 @@ enum class Outcome { Kept, Lost, Missed }; Outcome Run(const Case& test) { std::optional source; if (test.kind == Kind::Copied) source.emplace(); - Surface surface(test.kind == Kind::LeftWatch); + const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch; + Surface surface(leaves); auto* bytes = surface.Bytes(); const auto address = surface.Address(); std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); - bool unsaved = false; if (test.kind == Kind::Copied) { Store(source->Texels()); Settle(hold, *source, Stored); @@ -230,8 +230,12 @@ Outcome Run(const Case& test) { Settle(hold, *source, Filled); } Store(surface.Texels()); - if (test.kind == Kind::Filled) { + if (test.kind == Kind::Filled || leaves) { Settle(hold, surface, Stored); + if (leaves) { + std::vector settled(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, settled); + } Fill(surface.Texels()); if (!HoldPending(hold, surface, Filled)) return Outcome::Missed; } else if (test.kind == Kind::Copied) { @@ -249,7 +253,11 @@ Outcome Run(const Case& test) { } else if (!HoldPending(hold, surface, Stored)) { return Outcome::Missed; } - if (test.kind == Kind::LeftWatch) { + const auto writePattern = [&] { + for (std::size_t offset = 0; offset < Page; ++offset) static_cast(bytes)[offset] = Pattern(offset); + }; + if (test.kind == Kind::WrittenThenLeftWatch) writePattern(); + if (leaves) { if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { StorageTexture::FlushAllPending("store over untracked fixture"); std::fprintf(stderr, "[store over untracked] %s: not run, the surface is not write-watched\n", test.name); @@ -257,32 +265,21 @@ Outcome Run(const Case& test) { } const auto image = StorageTexture::FindLive(address, SurfaceBytes); Require(image != nullptr, "store over untracked: the surface has no storage image"); - unsaved = !image->GuestSnapshotValid(); - AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!image->GuestSnapshotValid(), "store over untracked: the pending result had saved guest bytes while its memory was write-watched"); + if (test.kind == Kind::ImportLeftWatch) Require(AgcDriver::GuestMemory::ImportWatched(address, SurfaceBytes, [] { return true; }, false), "store over untracked: the import that leaves the write watch failed"); + else AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); } std::array read{}; if (test.kind == Kind::ReadBefore) AgcDriver::GuestMemory::Read(address + Block + Page, read); - for (std::size_t offset = 0; offset < Page; ++offset) static_cast(bytes)[offset] = Pattern(offset); - if (unsaved) { - bool refused = false; - try { - AgcDriver::GuestMemory::Read(address + test.access, read); - } catch (const std::runtime_error& error) { - if (std::string_view(error.what()).find("outside the write watch") == std::string_view::npos) throw; - refused = true; - } - static_cast(StorageTexture::DiscardPendingInside(address, SurfaceBytes)); - if (refused && LostBytes(bytes) == 0) return Outcome::Kept; - std::fprintf(stderr, "[store over untracked] %s: the result was stored with neither write stamps nor saved bytes to tell what the CPU wrote\n", test.name); - return Outcome::Lost; - } + if (test.kind != Kind::WrittenThenLeftWatch) writePattern(); AgcDriver::GuestMemory::Read(address + test.access, read); const auto lostAtAccess = LostBytes(bytes); StorageTexture::FlushAllPending("store over untracked fixture"); std::vector whole(SurfaceBytes); AgcDriver::GuestMemory::Read(address, whole); const auto lostAtEnd = LostBytes(bytes); - if (test.kind == Kind::Filled || test.kind == Kind::Copied) Require(static_cast(surface.Texels())[2 * Block / 4u] == Filled, "store over untracked: the result did not reach a block the CPU left alone"); + if (test.kind == Kind::Filled || test.kind == Kind::Copied || leaves) Require(static_cast(surface.Texels())[2 * Block / 4u] == Filled, "store over untracked: the result did not reach a block the CPU left alone"); if (lostAtAccess == 0 && lostAtEnd == 0) return Outcome::Kept; std::fprintf(stderr, "[store over untracked] %s: of 0x%zx bytes the CPU wrote while the result was pending, %zu were lost after the access at +0x%zx and %zu after every result was stored\n", test.name, Page, lostAtAccess, test.access, lostAtEnd); return Outcome::Lost; @@ -303,12 +300,14 @@ int main() { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, {"an image copy into the surface", Kind::Copied, 0xe000}, {"a surface that left the write watch", Kind::LeftWatch, 0xe000}, + {"a surface written before it left the write watch", Kind::WrittenThenLeftWatch, 0xe000}, + {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7647ad114f..67236b6aa8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -49,7 +49,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill or the image copy. Results over a range that left the watch after they became pending (a host import that does not stay watched, made while they were pending) have no saved bytes, and storing them throws; saving those bytes or storing the results when the range leaves the watch is unimplemented. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. Units without a write generation, ranges that leave the watch through a failed collect, and ranges that leave it on a thread without the GPU lock while results are pending over them have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From 5894feff7e0e7074d67a50f39be341a9ce6ffa94 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Tue, 13 Oct 2026 13:41:16 -0300 Subject: [PATCH 284/353] feat(agc): import shared ranges above 64 MiB as sparse buffers over udmabuf chunks On Linux a shared-backed guest range is imported through one udmabuf covering the whole range, and udmabuf refuses buffers above its size_limit_mb (64 MiB by default). Larger ranges fell back to copies; an address-based dispatch then synced its whole mirror, storing back every pending storage image inside it each time. Such a range is now one sparse buffer bound to the existing 64 MiB chunk imports. The bind runs on a sparse binding queue of its own when the device has one, so imports made at map time (without the GPU mutex) can use it; otherwise only under the GPU mutex on the main queue. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 40 +++++ .../Graphics/include/Context.hpp | 6 + .../Graphics/src/GuestBufferMemory.cpp | 68 ++++++++ .../tests/execution/SparseDmaBufImport.cpp | 151 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 2 +- 6 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 98209ca72c..bc7e2ed338 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -304,6 +304,11 @@ struct VulkanDevice::State { bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; bool dmaBufImport = false; + bool sparseBinding = false; + std::uint32_t queueFamily = 0; + std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + VkQueue sparseQueue = VK_NULL_HANDLE; + std::mutex sparseQueueMutex; bool memoryBudget = false; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -1383,6 +1388,34 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; + VkDeviceQueueCreateInfo queueInfos[2]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; + if (available.sparseBinding && state->dmaBufImport) { + std::uint32_t families = 0; + auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); + getFamilies(selected, &families, nullptr); + std::vector queues(families); + getFamilies(selected, &families, queues.data()); + const auto binds = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; + std::uint32_t sparseFamily = families; + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i) && queues[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) sparseFamily = i; + } + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i)) sparseFamily = i; + } + if (sparseFamily != families) { + queueInfos[1].queueFamilyIndex = sparseFamily; + queueInfos[1].queueCount = 1; + queueInfos[1].pQueuePriorities = &priority; + deviceInfo.queueCreateInfoCount = 2; + state->sparseFamily = sparseFamily; + state->sparseBinding = true; + } else { + state->sparseBinding = family < families && (queues[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; + } + enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; + } + deviceInfo.pQueueCreateInfos = queueInfos; // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1637,6 +1670,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); + state->queueFamily = family; + if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast(state->device), reinterpret_cast(state->queue), family); VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; @@ -3146,6 +3181,11 @@ Graphics::Context VulkanDevice::buildContext() const { }; context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; context.dmaBufImport = state->dmaBufImport; + context.sparseBinding = state->sparseBinding; + context.queueFamily = state->queueFamily; + context.sparseFamily = state->sparseFamily; + context.sparseQueue = state->sparseQueue; + context.sparseQueueMutex = &state->sparseQueueMutex; context.recorder = state->recorder.get(); context.descriptorCache = state->descriptorCache.get(); context.samplerCache = state->samplerCache.get(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c83858906b..1ebf013ed6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -4,6 +4,7 @@ #ifndef VK_NO_PROTOTYPES #define VK_NO_PROTOTYPES #endif +#include #include #include #include @@ -138,6 +139,11 @@ struct Context { // Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment. VkDeviceSize hostImportAlignment = 0; bool dmaBufImport = false; + bool sparseBinding = false; + std::uint32_t queueFamily = 0; + std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + VkQueue sparseQueue = VK_NULL_HANDLE; + std::mutex* sparseQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 8f0be0a68e..d8c80f860b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -481,6 +481,70 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, entry.dmaBuf = true; return nullptr; } + +bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; + if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread())) return false; + const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + info.flags = VK_BUFFER_CREATE_SPARSE_BINDING_BIT; + info.size = entry.bytes; + info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + const std::uint32_t families[] = {context.queueFamily, context.sparseFamily}; + info.sharingMode = ownQueue ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = ownQueue ? 2u : 0u; + info.pQueueFamilyIndices = ownQueue ? families : nullptr; + VkBuffer buffer = VK_NULL_HANDLE; + if (context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer) != VK_SUCCESS) return false; + const auto destroy = [&] { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + return false; + }; + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + const auto alignment = requirements.alignment; + if (alignment == 0 || offset % alignment != 0 || ImportChunkBytes % alignment != 0) return destroy(); + auto chunks = std::make_shared>>(); + std::vector binds; + for (VkDeviceSize resource = 0; resource < requirements.size;) { + const auto position = offset + resource; + const auto index = position / ImportChunkBytes; + const auto within = position - index * ImportChunkBytes; + const auto size = std::min(ImportChunkBytes - within, requirements.size - resource); + const char* step = nullptr; + VkResult failure = VK_SUCCESS; + auto chunk = importChunk(context, file, index, requirements.memoryTypeBits, step, failure); + if (chunk == nullptr || ((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) return destroy(); + binds.push_back({resource, size, chunk->memory, within, 0}); + chunks->push_back(std::move(chunk)); + resource += size; + } + const VkSparseBufferMemoryBindInfo bufferBinds{buffer, static_cast(binds.size()), binds.data()}; + VkBindSparseInfo bind{VK_STRUCTURE_TYPE_BIND_SPARSE_INFO}; + bind.bufferBindCount = 1; + bind.pBufferBinds = &bufferBinds; + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + if (context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) return destroy(); + VkResult waited = VK_SUCCESS; + { + std::unique_lock queueLock; + if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex); + const auto bound = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); + waited = bound == VK_SUCCESS ? context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX) : bound; + } + context.Function("vkDestroyFence")(context.device, fence, nullptr); + if (waited != VK_SUCCESS) return destroy(); + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + if (address == 0) return destroy(); + entry.buffer = buffer; + entry.memory = VK_NULL_HANDLE; + entry.address = address; + entry.chunk = std::move(chunks); + entry.dmaBuf = true; + return true; +} #endif #ifndef _WIN32 @@ -507,6 +571,10 @@ const char* createBackingImport(const Context& context, HostImport& entry, Backi close(file); return nullptr; } + if (!fits && createSparseDmaBufImport(context, entry, file, backing.offset)) { + close(file); + return nullptr; + } return createDmaBufImport(context, entry, file, backing.offset, failure); } #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp new file mode 100644 index 0000000000..e3760335ee --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifndef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return VulkanTestSkipped; +} +#else +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; + +std::uint8_t* mapping = nullptr; +std::uint64_t mappingBytes = 0; +int backing = -1; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + const auto begin = reinterpret_cast(mapping); + if (address < begin || address + bytes > begin + mappingBytes) return false; + *file = dup(backing); + *offset = address - begin; + return *file >= 0; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device; + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); +} + +} + +int main() { + try { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, /dev/udmabuf is not accessible\n"); + return VulkanTestSkipped; + } + close(udmabuf); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + + constexpr std::size_t ProbeBytes = 65536; + auto* probe = static_cast(std::aligned_alloc(ProbeBytes, ProbeBytes)); + Require(probe != nullptr, "sparse import: cannot allocate the probe"); + GuestAllocations::Mutation().Add(probe, ProbeBytes, true, true, true); + bool adopted = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{1u, 2u, 3u, 4u}; + adopted = device->FillBuffer(reinterpret_cast(probe), ProbeBytes, pattern); + device->WaitIdle(); + } + if (!adopted) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + + const auto bytes = (UdmabufLimitBytes() / ChunkBytes + 2) * ChunkBytes; + mappingBytes = bytes + ChunkBytes; + backing = memfd_create("anyps5-sparse-import-test", MFD_CLOEXEC | MFD_ALLOW_SEALING); + Require(backing >= 0 && ftruncate(backing, static_cast(mappingBytes)) == 0, "sparse import: cannot make the backing file"); + Require(fcntl(backing, F_ADD_SEALS, F_SEAL_SHRINK) == 0, "sparse import: cannot seal the backing file"); + void* mapped = mmap(nullptr, mappingBytes, PROT_READ | PROT_WRITE, MAP_SHARED, backing, 0); + Require(mapped != MAP_FAILED, "sparse import: cannot map the backing file"); + mapping = static_cast(mapped); + GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); + + const auto Register = [](std::uint8_t* memory, std::uint64_t size) { + GuestAllocations::Mutation().Add(memory, static_cast(size), true, true, true); + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == reinterpret_cast(memory)) ranges.push_back(range); + } + Require(ranges.size() == 1, "sparse import: the allocation is not registered"); + return ranges; + }; + + auto* small = mapping + bytes; + constexpr std::uint64_t SmallBytes = std::uint64_t{1} << 20u; + device->ImportGuestMemory(Register(small, SmallBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + const bool smallImported = Imported(device->Device(), reinterpret_cast(small), static_cast(SmallBytes)); + GuestAllocations::Mutation().Remove(small); + if (!smallImported) { + std::printf("skipped, the device does not import dma-buf memory\n"); + return VulkanTestSkipped; + } + + const auto boundary = ChunkBytes; + std::memset(mapping + boundary - 65536, 0x11, 131072); + const auto ranges = Register(mapping, bytes); + const auto address = reinterpret_cast(mapping); + const bool atMap = device->ImportGuestMemory(ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + Require(Imported(device->Device(), address, static_cast(bytes)), "sparse import: a range larger than udmabuf's size limit was not imported"); + Require(atMap, "sparse import: the import made without the GPU lock was refused"); + + bool filled = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du}; + filled = device->FillBuffer(address + boundary - 32768, 65536, pattern); + device->WaitIdle(); + } + Require(filled, "sparse import: the fill across the chunk boundary was not served by the import"); + for (std::uint64_t offset = boundary - 32768; offset < boundary + 32768; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, mapping + offset, sizeof(value)); + Require(value == 0x5EEDF00Du, "sparse import: the GPU fill did not reach the backing memory across the chunk boundary"); + } + Require(mapping[boundary - 32769] == 0x11 && mapping[boundary + 32768] == 0x11, "sparse import: the GPU fill reached past its range"); + + GuestAllocations::Mutation().Remove(mapping); + GuestAllocations::Mutation().Remove(probe); + std::free(probe); + std::cout << "AGC sparse dma-buf import tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} +#endif diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7647ad114f..4f638514c4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -52,6 +52,7 @@ - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill or the image copy. Results over a range that left the watch after they became pending (a host import that does not stay watched, made while they were pending) have no saved bytes, and storing them throws; saving those bytes or storing the results when the range leaves the watch is unimplemented. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. +- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. A range whose offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV) still takes the single udmabuf import, which udmabuf's `size_limit_mb` refuses above 64 MiB by default, and is copied; the NVIDIA proprietary driver has no memory type shared by dma-buf memory and sparse buffers and imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [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. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index e52f0383bf..1463dbcb74 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -99,7 +99,7 @@ On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.d On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. -On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. Ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. +On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the range does not start on the device's sparse block size in the backing file, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. Linux: From c58005374fd84744c1bb9d3f63cc852a2e42ec24 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 17:56:28 +1300 Subject: [PATCH 285/353] fix(libkernel): name getdtablesize's throw with __func__ --- core/libs/prx/libkernel/System/src/Process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 792d852ddb..2d13a53816 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -568,7 +568,7 @@ int APS5_VABI getpriority_nid_postfix(int which, int who) { } int APS5_VABI getdtablesize_nid_postfix(void) { - NotImplemented_nid_no_patch("getdtablesize: the console's descriptor limit"); + NotImplemented_nid_no_patch(__func__); return -1; } From 26a746a433742788d4e1eb5e057af64c210249ac Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 13:21:24 +0200 Subject: [PATCH 286/353] fix(shader): take the 32_AR export alpha from channel 1 as GFX10 does A pixel export to an MRT whose SPI_SHADER_COL_FORMAT is 32_AR read its alpha from export channel 3 (EN mask 0x9), the layout before GFX10. On GFX10, which the PS5's gfx1013 is, the alpha is channel 1: Mesa's emit_ps_color_export (ac_nir_lower_ps_late.c, index gfx_level >= GFX10 ? 1 : 3), ACO and radeonsi in Mesa 22.0 (enabled_channels 0x3 with the alpha in channel 1) and LLPC's FragmentColorExport.cpp (EXP_FORMAT_32_AR, exports[1]) all emit it there. A console shader's `exp mrt0 v0, v1, off, off` therefore lost its alpha: it stayed at the 1.0 default, so an alpha-only COLOR_32 ALT_REV target stored 1.0 and an R32 target blended with SRC_ALPHA blended as if the alpha were 1. With channel 3 enabled, channel 3 was taken instead. ExportVector now reads channels 0 and 1 for 32_AR and puts channel 1 in the alpha component; channels 2 and 3 are ignored. The layout comes from the compilers, not from a hardware measurement, and is recorded in docs/dev/debt/shader.md. agc_driver_pixel32_ar_export draws 32_AR exports with EN 0x3, 0xb and 0xf, as wave32 and wave64, into a COLOR_32 FLOAT ALT_REV target and a COLOR_32 FLOAT target blended with SRC_ALPHA and ONE_MINUS_SRC_ALPHA, and compares every stored value bit for bit. Without the fix every row fails: the stored value uses an alpha of 1.0 or of channel 3. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/Pixel32ArExport.cpp | 299 ++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 4 +- docs/dev/debt/shader.md | 1 + 3 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp new file mode 100644 index 0000000000..daaad752c8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t ElementBytes = 4; +constexpr std::size_t RowBytes = std::size_t{Width} * ElementBytes; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float32Info = 0x0710u; +constexpr std::uint32_t Float32AlternateReversedInfo = 0x1f10u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Export32Ar = 3; +constexpr float Destination = 0.75f; + +struct Row { + std::uint32_t enables; + std::array sources; +}; + +constexpr std::array Rows{{ + {0x3u, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xbu, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xfu, {0.125f, 0.5f, 2.0f, 0.0625f}}, + {0x3u, {0.25f, 0.75f, 2.0f, 0.0625f}}, +}}; +constexpr std::uint32_t Height = Rows.size() + 1; +constexpr std::size_t TargetBytes = RowBytes * Height; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Rows.size()> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index) { + auto& code = Programs[index]; + const auto& row = Rows[index]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(row.sources[channel]); + } + code[at++] = 0xf8001800u | row.enables; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + return AgcDriver::Graphics::DecodeColorBuffer(cx, 0); +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::uint32_t waveSize, std::size_t row) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, static_cast(row)}, {Width, 1}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto pixelCode = Program(row); + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, waveSize == 32u ? 0x8000u : 0u}, {0x1c5, Export32Ar}, {0x203, 0u}}; + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.targetOutputMode[0] == Export32Ar && pixel.wave32 == (waveSize == 32u), "the pixel registers did not decode as a 32_AR MRT0 export"); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes / sizeof(std::uint32_t)); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Describe(const Row& row) { + char text[96]; + std::snprintf(text, sizeof(text), "EN 0x%x of (%g, %g, %g, %g)", row.enables, row.sources[0], row.sources[1], row.sources[2], row.sources[3]); + return text; +} + +struct TargetCase { + const char* name; + std::uint32_t info; + std::uint8_t mapping; + bool blended; +}; + +void ExportTests(AgcDriver::VulkanDevice& device, const TargetCase& test, std::uint32_t waveSize) { + Block block(BlockBytes); + std::vector filled(BlockBytes / sizeof(std::uint32_t), std::bit_cast(Destination)); + std::memcpy(block.data, filled.data(), BlockBytes); + const auto color = DecodeTarget(block, test.info); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.componentMapping == test.mapping && color.elementBytes == ElementBytes, std::string("the ") + test.name + " target did not decode as R32_SFLOAT with component mapping " + Hex(test.mapping)); + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, ElementBytes).Bytes() == TargetBytes, "the linear COLOR_32 target is padded"); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + if (test.blended) { + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorBlendOp = VK_BLEND_OP_ADD; + blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blend.alphaBlendOp = VK_BLEND_OP_ADD; + } + for (std::size_t row = 0; row < Rows.size(); ++row) Draw(device, color, blend, waveSize, row); + const auto stored = ReadBack(device, block.Address(), TargetBytes); + for (std::size_t row = 0; row < Height; ++row) { + float expected = Destination; + if (row < Rows.size()) { + const auto red = Rows[row].sources[0]; + const auto alpha = Rows[row].sources[1]; + expected = test.blended ? red * alpha + Destination * (1.0f - alpha) : alpha; + } + for (std::uint32_t x = 0; x < Width; ++x) { + const auto actual = stored[row * Width + x]; + if (actual == std::bit_cast(expected)) continue; + const auto source = row < Rows.size() ? "the 32_AR export with " + Describe(Rows[row]) : std::string("a row outside the drawn rows"); + throw std::runtime_error("wave" + std::to_string(waveSize) + " " + source + " into the " + test.name + " target stored " + Hex(actual) + " at pixel (" + std::to_string(x) + ", " + std::to_string(row) + "), expected " + Hex(std::bit_cast(expected))); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + constexpr std::array targets{{ + {"COLOR_32 FLOAT ALT_REV (alpha only)", Float32AlternateReversedInfo, 0xe7u, false}, + {"COLOR_32 FLOAT STD blended with SRC_ALPHA and ONE_MINUS_SRC_ALPHA", Float32Info, 0xe4u, true}, + }}; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const auto& test : targets) ExportTests(*device, test, waveSize); + } + std::puts("32_AR export tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index c9914f8551..e4bd7673ac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -337,7 +337,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; const auto mode = MrtOutputMode(state, exp); - const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); + const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u || mode == 3u ? 0x3u : 0xfu); if (exp.compr) { for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -356,7 +356,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const } else { for (std::uint32_t component = 0; component < 4u; component++) { if (((exported >> component) & 1u) != 0u) { - raw.at(component) = ExportRawComponent(ctx, data, component); + raw.at(mode == 3u && component == 1u ? 3u : component) = ExportRawComponent(ctx, data, component); } } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 03725ac06a..1c086112cc 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -14,6 +14,7 @@ - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them, so a shader with eight guest clip and cull distances exceeds the eight the device holds and fails to compile. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan +- [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) ### Functional From c8662b6bcace7648fe077f80b039dba68c7113fa Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:10:53 +0100 Subject: [PATCH 287/353] fix(agc): accept alpha-to-coverage from a 32_AR MRT0 export without a color target radeonsi exports MRT0 as 32_AR when alpha-to-coverage is on and no color buffer is bound, so a depth-only draw can depend on that alpha. The decoder now accepts 32_AR and an absent MRT0 target, and reads the MRT0 export format from SPI_SHADER_COL_FORMAT, which it masked to 0 for draws without a color target. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 7 +-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++- .../tests/execution/AlphaToCoverage.cpp | 61 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 2a7172224d..8bf6f73eb9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -985,10 +985,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } if (result.alphaToCoverage) { const auto mrt0 = std::find_if(result.colors.begin(), result.colors.end(), [](const ColorTarget& color) { return color.exportIndex == 0; }); - Require(mrt0 != result.colors.end(), "alpha-to-coverage without a written MRT0 color target is unsupported"); - const auto format = exportFormat & 0xfu; - Require(format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); - Require(mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None, "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); + const auto format = read(cx, 0x1c5) & 0xfu; + Require(format == 3u || format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); + Require(mrt0 == result.colors.end() || (mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None), "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); } APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast(result.topology)); return result; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c31f7f4000..24ca79907c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2637,14 +2637,23 @@ void AlphaToCoverageTests() { } queue.context[0x2dc] = 0xaa01; const std::uint32_t exportFormat = queue.context.at(0x1c5); - for (const auto format : {4u, 5u, 6u, 9u}) { + for (const auto format : {3u, 4u, 5u, 6u, 9u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha"); } - for (const auto format : {1u, 2u, 3u}) { + for (const auto format : {1u, 2u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); } + const auto targetMask = queue.context.at(0x8e); + queue.context[0x8e] = 0; + queue.context[0x1c5] = (exportFormat & ~0xfu) | 3u; + const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); + Require(depthOnly.alphaToCoverage && depthOnly.colors.empty(), "alpha-to-coverage without a written color target and a 32_AR MRT0 export was refused or kept a target"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected alpha-to-coverage without a written color target"); + queue.context[0x1c5] = exportFormat & ~0xfu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); + queue.context[0x8e] = targetMask; queue.context[0x1c5] = exportFormat; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 288ca4e06c..610161bced 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "Recompiler.hpp" #include "Optimization/ResourceProgram.hpp" +#include "DepthFastClearHarness.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -255,6 +256,59 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge device.Draw(state, draw, shaders); } +alignas(256) std::array, 3> DepthOnlyPrograms{}; + +void DepthOnlyCoverage(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& harness, const Block& depth, float alpha, std::size_t slot, bool written, std::uint32_t enable = 0x3u, float fourth = 0.0f) { + namespace Harness = DepthFastClearHarness; + const Harness::Surface surface{depth.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + auto& code = DepthOnlyPrograms[slot]; + code = {0x7e0802ffu, 0x3e800000u, 0x7e0a02ffu, std::bit_cast(alpha), 0x7e0e02ffu, std::bit_cast(fourth), 0xf8001800u | enable, 0x07060504u, 0xbf810000u}; + const std::span pixelCode(code.data(), 9); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 3; + pixel.targetExportMapping.fill(0xe4u); + pixel.alphaToCoverage = true; + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {32, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(harness.vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &harness.vertex, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(harness.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.alphaToCoverage = true; + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Harness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + const auto passed = Harness::Draw(device, harness, surface, {.depthCompare = VK_COMPARE_OP_LESS}); + char detail[160]; + std::snprintf(detail, sizeof(detail), "a depth-only draw with alpha-to-coverage and a 32_AR alpha of %.2f (export mask 0x%x, fourth channel %.2f) left %u of %u pixels at the cleared depth", alpha, enable, fourth, passed, Harness::Covered); + Require(passed == (written ? 0u : Harness::Covered), detail); +} + std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); device.WaitIdle(); @@ -343,6 +397,13 @@ int main() { Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, name); programCount = 0; } + const auto harness = DepthFastClearHarness::Compile(*device); + const Block coveredDepth(BlockBytes); + const Block discardedDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, coveredDepth, 0.75f, 0, true); + DepthOnlyCoverage(*device, harness, discardedDepth, 0.25f, 1, false); + const Block fullMaskDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, fullMaskDepth, 0.25f, 2, false, 0xfu, 0.75f); std::puts("alpha-to-coverage tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7647ad114f..2fb983753f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -7,7 +7,7 @@ - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [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 +- [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. - [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 - [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 From 8d89265c7af16b52e8fa3f1d9166dfa9c35ea6b2 Mon Sep 17 00:00:00 2001 From: MathiasDevelopes <25400788+MathiasDevelopes@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:04:15 +0200 Subject: [PATCH 288/353] fix(agc): initialize stencil control and reference masks (cherry picked from commit 85e0d98bba896002539365bb29a7a39a8e43f501) --- .../Execution/include/QueueState.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 +++++++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 3 +++ docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 27 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index fabe4a59a6..0d6a6da583 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -226,6 +226,7 @@ inline Registers BuildInitialContextRegisters() { {0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000}, {0x90, 0x80000000}, {0x91, 0x40004000}, {0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0}, + {0x10b, 0}, {0x10c, 0}, {0x10d, 0}, {0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0}, {0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0}, {0x2ce, 0}, {0x2d3, 0}, {0x2d5, 0}, {0x2d6, 0}, {0x2db, 0}, diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c31f7f4000..38d51afeec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1370,6 +1370,28 @@ void DepthStencilTests() { for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; queue.context[0x005] = 0x300; queue.context[0x01e] = 0x1; + { + AgcDriver::QueueState unwritten; + unwritten.shader = queue.shader; + unwritten.userConfig = queue.userConfig; + queue.context[0x200] = 0x00700791; + for (int reset = 0; reset < 2; ++reset) { + queue.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { unwritten.context[offset] = value; }); + const auto state = AgcDriver::Graphics::DecodeState(unwritten); + Require(state.stencilTest && state.depth, "unwritten stencil registers rejected a stencil draw"); + for (const auto face : {state.stencilFront, state.stencilBack}) { + Require(face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP && face.compareOp == VK_COMPARE_OP_ALWAYS, "unwritten stencil operations changed"); + Require(face.compareMask == 0 && face.writeMask == 0 && face.reference == 0, "unwritten stencil reference or masks changed"); + } + Require(AgcDriver::Graphics::DrawRejection(unwritten, false).empty(), "precheck rejected unwritten stencil registers"); + unwritten.context[0x10b] = 0x00070070; + unwritten.context[0x10c] = unwritten.context[0x10d] = 0x01ffff09; + const auto explicitState = AgcDriver::Graphics::DecodeState(unwritten); + for (const auto face : {explicitState.stencilFront, explicitState.stencilBack}) Require(face.passOp == VK_STENCIL_OP_INVERT && face.compareMask == 0xff && face.writeMask == 0xff && face.reference == 9, "explicit stencil state was replaced by defaults"); + unwritten.ClearContext(); + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) Require(unwritten.context.at(offset) == 0, "context clear did not reset stencil state"); + } + } queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x01ffff01; queue.context[0x10d] = 0x01000001; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 490ba22a37..e54aeab924 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -168,6 +168,9 @@ void testRegisterFile() { void testContextAndBases() { AgcDriver::QueueState state; + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu})); + execute(state, makePacket(0x12, {0})); + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults"); execute(state, makePacket(0x69, {0x10, 17})); execute(state, makePacket(0x76, {0x20c, 2})); execute(state, makePacket(0x10, {3, 0}, 0x68)); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7647ad114f..7f54a1f6b5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -29,6 +29,7 @@ ### Functional +- [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. - [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. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, or a sampled copy) is a separate image, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From db4fd5f29c3bfc4aad9693e0286d99815644cc1c Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 18:04:20 +1300 Subject: [PATCH 289/353] fix(agc): take the GPU lock when saving pending storage results before the range leaves the write watch A draw made outside the GPU lock (agc_driver_recorded_kept_bytes) imports a target's memory and leaves the watch there; the hook threw instead of saving. It now takes the lock, as FlushPending does. --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 3f89f804c4..9722102c0c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3184,7 +3184,7 @@ void StorageTexture::SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes) for (auto* texture : pending.flushing) take(texture); } if (overlapping.empty()) return; - Require(GuestMemory::GpuMutex().HeldByThisThread(), "AGC graphics: memory under pending storage image results leaves the write watch on a thread without the GPU lock"); + std::lock_guard gpu(GuestMemory::GpuMutex()); for (const auto& texture : overlapping) texture->saveBeforeUnwatch(); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 67236b6aa8..6dc95f6bf4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -49,7 +49,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. Units without a write generation, ranges that leave the watch through a failed collect, and ranges that leave it on a thread without the GPU lock while results are pending over them have no saved bytes; storing such results throws. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. Units without a write generation and ranges that leave the watch through a failed collect have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From 975ed1213df97feeeacd7fd270a7794d2b9d0a41 Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Mon, 12 Oct 2026 19:40:50 +0000 Subject: [PATCH 290/353] feat(libkernel): count committed ranges in sceKernelGetPageTableStats Charge each recorded protection ranges 2 MiB granules to the gpu pool when the range has GPU-access protection bits and to the cpu pool otherwise, so the available counts reflect committed usage instead of staying at the 2048-entry budget. Follows KytyPS5s counting model. --- core/libs/prx/libkernel/CMakeLists.txt | 9 ++ .../libkernel/DirectMemory/DirectMemory.cpp | 14 ++ .../libkernel/DirectMemory/DirectMemory.hpp | 1 + .../prx/libkernel/DirectMemory/Export.cpp | 14 +- core/libs/tests/GuestPageTableStats.cpp | 138 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 6 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestPageTableStats.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6d9e5fbdc6..6978dca141 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1203,6 +1203,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_page_table_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestPageTableStats.cpp) + target_include_directories(guest_page_table_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_page_table_stats_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_page_table_stats_tests) + add_test(NAME guest_page_table_stats COMMAND guest_page_table_stats_tests) + if(WIN32) + set_tests_properties(guest_page_table_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_mlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestMlock.cpp) target_include_directories(guest_mlock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mlock_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index dc3a1bbcfd..a328556d73 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1168,6 +1168,20 @@ bool GuestProtection(uintptr_t addr, int* prot) { return true; } +void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpuEntries, std::uint64_t* gpuEntries) { + std::uint64_t cpu = 0; + std::uint64_t gpu = 0; + std::lock_guard lock(g_protectionLock); + for (const auto& [start, range] : g_protections) { + if (range.end <= start) continue; + const auto entries = (range.end - 1) / granularity - start / granularity + 1; + if (range.prot & 0x7) cpu += entries; + if (range.prot & GuestProtGpuReadWrite) gpu += entries; + } + *cpuEntries = cpu; + *gpuEntries = gpu; +} + bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType) { std::lock_guard lock(g_directLock); const auto next = g_directMappings.upper_bound(address); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index c611faeabf..0307c36cab 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -47,5 +47,6 @@ bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end); void ForgetJitSharedMemory(int descriptor); void DuplicateJitSharedMemory(int from, int to); +void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpu_entries, std::uint64_t* gpu_entries); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index e8baaf3781..dca35ab051 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -411,11 +411,15 @@ int APS5_VABI sceKernelClearVirtualRangeName(const void* addr, uint64_t len) { int APS5_VABI sceKernelGetPageTableStats(int* cpu_total, int* cpu_available, int* gpu_total, int* gpu_available) { if (!cpu_total || !cpu_available || !gpu_total || !gpu_available) return SCE_KERNEL_ERROR_EFAULT; - constexpr int pageTableEntries = static_cast((4ull << 30) / (2ull << 20)); - *cpu_total = pageTableEntries; - *cpu_available = pageTableEntries; - *gpu_total = pageTableEntries; - *gpu_available = pageTableEntries; + constexpr std::uint64_t PageTableGranularity = 2ULL * 1024 * 1024; + constexpr std::uint64_t PageTablePoolEntries = 2048; + std::uint64_t cpuUsed = 0; + std::uint64_t gpuUsed = 0; + GuestPageTableUsage(PageTableGranularity, &cpuUsed, &gpuUsed); + *cpu_total = static_cast(PageTablePoolEntries); + *gpu_total = static_cast(PageTablePoolEntries); + *cpu_available = static_cast(cpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - cpuUsed); + *gpu_available = static_cast(gpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - gpuUsed); return 0; } diff --git a/core/libs/tests/GuestPageTableStats.cpp b/core/libs/tests/GuestPageTableStats.cpp new file mode 100644 index 0000000000..6a47951c8a --- /dev/null +++ b/core/libs/tests/GuestPageTableStats.cpp @@ -0,0 +1,138 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelGetPageTableStats(int*, int*, int*, int*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Page table stats check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +constexpr std::size_t PageTableGranule = 0x200000; +constexpr int PageTableTotal = 2048; + +struct Stats { + int cpuTotal; + int cpuAvailable; + int gpuTotal; + int gpuAvailable; +}; + +static Stats Query() { + Stats stats{}; + Require(sceKernelGetPageTableStats(&stats.cpuTotal, &stats.cpuAvailable, &stats.gpuTotal, &stats.gpuAvailable) == 0); + return stats; +} + +static int Granules(const void* addr, std::size_t len) { + const auto start = reinterpret_cast(addr); + return static_cast((start + len - 1) / PageTableGranule - start / PageTableGranule + 1); +} + +static void CheckRejectedArguments() { + int value = 0; + Require(sceKernelGetPageTableStats(nullptr, &value, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, nullptr, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, nullptr, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, &value, nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void CheckFlexibleMapping() { + const auto before = Query(); + Require(before.cpuTotal == PageTableTotal && before.gpuTotal == PageTableTotal); + Require(before.cpuAvailable <= before.cpuTotal && before.gpuAvailable <= before.gpuTotal); + + constexpr std::size_t len = 3 * PageTableGranule; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, len, 3, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(during.gpuAvailable == before.gpuAvailable); + + Require(sceKernelMprotect(mapped, len, 0x30) == 0); + during = Query(); + Require(during.cpuAvailable == before.cpuAvailable); + Require(during.gpuAvailable == before.gpuAvailable - entries); + + Require(sceKernelMunmap(mapped, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckDualAccessMapping() { + const auto before = Query(); + constexpr std::size_t len = PageTableGranule; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, len, 0x33, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + const auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(during.gpuAvailable == before.gpuAvailable - entries); + Require(sceKernelMunmap(mapped, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckSharedGranule() { + const auto before = Query(); + constexpr std::size_t page = 0x4000; + void* span = nullptr; + Require(sceKernelReserveVirtualRange(&span, 2 * PageTableGranule, 0, PageTableGranule) == 0 && span != nullptr); + const auto base = reinterpret_cast(span); + const auto aligned = base % PageTableGranule == 0 ? base : (base / PageTableGranule + 1) * PageTableGranule; + void* first = reinterpret_cast(aligned); + void* second = reinterpret_cast(aligned + page); + Require(sceKernelMapFlexibleMemory(&first, page, 3, 0) == 0 && first == reinterpret_cast(aligned)); + Require(sceKernelMapFlexibleMemory(&second, page, 3, 0) == 0 && second == reinterpret_cast(aligned + page)); + const auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - 2); + Require(during.gpuAvailable == before.gpuAvailable); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckGpuDirectMapping() { + const auto before = Query(); + constexpr std::size_t len = 2 * PageTableGranule; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, len, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, len, 0x33, 0, phys, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + const auto during = Query(); + Require(during.gpuAvailable == before.gpuAvailable - entries); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(sceKernelMunmap(mapped, len) == 0); + Require(sceKernelReleaseDirectMemory(phys, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +int main() { + CheckRejectedArguments(); + CheckFlexibleMapping(); + CheckDualAccessMapping(); + CheckSharedGranule(); + CheckGpuDirectMapping(); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index cfdc0680d8..67d94ddd5c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -177,7 +177,7 @@ - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) -- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up +- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports page-table entries over the recorded protection ranges (committed mappings; reservations are not recorded): each range charges one entry per 2 MiB granule it spans to the cpu pool when it has CPU access bits (0x7) and to the gpu pool when it has GPU access bits (0x30), so a range with both counts in both and two ranges sharing one granule charge it once each, as KytyPS5 sums per committed range. Mappings with no access bits, such as PROT_NONE or ACP-only protections, charge neither pool. The 2048-entry budget per side, its constants, and whether the console counts reservations or guest image segments are unverified. Resonance: A Plague Tale Legacy (PPSA30601) reaches this call after the other missing exports of its start-up are provided - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported From 4e136dd3230af4ac50ef1d20f37d959af32153b5 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 18:03:03 +1300 Subject: [PATCH 291/353] fix(agc): decide the sparse dma-buf import before importing and throw after A shared range larger than one chunk now takes the sparse path only when the device binds sparse memory, udmabuf is open, the range's backing offset is on the sparse block size, the sparse buffer accepts every memory type a chunk import can have and the backing file covers the last chunk. All of that is known before a chunk is imported. Once the path is chosen, a failed chunk import, fence creation, bind, fence wait or device address throws instead of going on to the whole-range udmabuf and copies. agc_host_import_sparse checks the import against a mocked device with the real udmabuf: a range above size_limit_mb becomes one sparse buffer bound to one chunk import per 64 MiB, an unaligned backing offset imports no chunk, and failures of UDMABUF_CREATE and vkQueueBindSparse after the decision throw. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 15 + .../Graphics/src/GuestBufferMemory.cpp | 89 +++-- .../tests/HostImportSparseDmaBuf.cpp | 307 ++++++++++++++++++ .../tests/execution/SparseDmaBufImport.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 381 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4b3f2ae5c1..e95c9dd8b2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1038,6 +1038,21 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_host_import_sparse_tests EXCLUDE_FROM_ALL tests/HostImportSparseDmaBuf.cpp) +target_include_directories(agc_host_import_sparse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_host_import_sparse_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_host_import_sparse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_host_import_sparse_tests) +if(BUILD_TESTING) + set_target_properties(agc_host_import_sparse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_host_import_sparse COMMAND agc_host_import_sparse_tests) + set_tests_properties(agc_host_import_sparse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_host_import_sparse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_presentation_pass_tests EXCLUDE_FROM_ALL tests/PresentationPass.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index d8c80f860b..4bcbc73bcc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -340,6 +340,7 @@ const char* createDmaBufImport(const Context& context, HostImport& entry, int fi #ifndef _WIN32 constexpr std::uint64_t ImportChunkBytes = std::uint64_t{64} << 20u; +constexpr VkBufferUsageFlags ChunkImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; struct ImportChunk { VkDevice device = VK_NULL_HANDLE; @@ -442,7 +443,7 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; info.size = entry.bytes; - info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; + info.usage = ChunkImportUsage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) { @@ -482,28 +483,56 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, return nullptr; } -bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { - const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; - if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread())) return false; +constexpr VkBufferUsageFlags SparseImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + +[[noreturn]] void throwSparseImport(const HostImport& entry, const char* step, VkResult result) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: sparse dma-buf import of 0x%llx+0x%llx failed at %s (%d)", static_cast(entry.base), static_cast(entry.bytes), step, static_cast(result)); + throw std::runtime_error(text); +} + +VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const HostImport& entry, VkBufferCreateFlags flags, VkBufferUsageFlags usage, VkDeviceSize size, VkBuffer* kept) { const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; - info.flags = VK_BUFFER_CREATE_SPARSE_BINDING_BIT; - info.size = entry.bytes; - info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + info.flags = flags; + info.size = size; + info.usage = usage; const std::uint32_t families[] = {context.queueFamily, context.sparseFamily}; - info.sharingMode = ownQueue ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = ownQueue ? 2u : 0u; - info.pQueueFamilyIndices = ownQueue ? families : nullptr; + const bool concurrent = (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0 && context.sparseQueue != VK_NULL_HANDLE; + info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = concurrent ? 2u : 0u; + info.pQueueFamilyIndices = concurrent ? families : nullptr; VkBuffer buffer = VK_NULL_HANDLE; - if (context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer) != VK_SUCCESS) return false; - const auto destroy = [&] { - context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); - return false; - }; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) throwSparseImport(entry, "vkCreateBuffer", result); VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (kept != nullptr) { + *kept = buffer; + } else { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + } + return requirements; +} + +bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; + if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread()) || udmabufDevice() < 0) return false; + struct stat backing {}; + if (fstat(file, &backing) != 0) throwSparseImport(entry, "fstat of the shared backing", VK_ERROR_INVALID_EXTERNAL_HANDLE); + const auto chunkTypes = dmaBufBufferRequirements(context, entry, 0, ChunkImportUsage, ImportChunkBytes, nullptr).memoryTypeBits; + VkBuffer buffer = VK_NULL_HANDLE; + const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, entry.bytes, &buffer); const auto alignment = requirements.alignment; - if (alignment == 0 || offset % alignment != 0 || ImportChunkBytes % alignment != 0) return destroy(); + const bool chosen = alignment != 0 && offset % alignment == 0 && ImportChunkBytes % alignment == 0 && (chunkTypes & ~requirements.memoryTypeBits) == 0 && (requirements.memoryTypeBits & chunkTypes) != 0 && + (offset + requirements.size + ImportChunkBytes - 1) / ImportChunkBytes * ImportChunkBytes <= static_cast(backing.st_size); + const auto fail = [&](const char* step, VkResult result) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + throwSparseImport(entry, step, result); + }; + if (!chosen) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + return false; + } auto chunks = std::make_shared>>(); std::vector binds; for (VkDeviceSize resource = 0; resource < requirements.size;) { @@ -514,7 +543,8 @@ bool createSparseDmaBufImport(const Context& context, HostImport& entry, int fil const char* step = nullptr; VkResult failure = VK_SUCCESS; auto chunk = importChunk(context, file, index, requirements.memoryTypeBits, step, failure); - if (chunk == nullptr || ((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) return destroy(); + if (chunk == nullptr) fail(step, failure); + if (((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) fail("the memory type of a chunk import", VK_ERROR_FORMAT_NOT_SUPPORTED); binds.push_back({resource, size, chunk->memory, within, 0}); chunks->push_back(std::move(chunk)); resource += size; @@ -525,19 +555,21 @@ bool createSparseDmaBufImport(const Context& context, HostImport& entry, int fil bind.pBufferBinds = &bufferBinds; const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; VkFence fence = VK_NULL_HANDLE; - if (context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) return destroy(); + if (const auto result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence); result != VK_SUCCESS) fail("vkCreateFence", result); + VkResult bound = VK_SUCCESS; VkResult waited = VK_SUCCESS; { std::unique_lock queueLock; if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex); - const auto bound = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); - waited = bound == VK_SUCCESS ? context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX) : bound; + bound = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); + if (bound == VK_SUCCESS) waited = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX); } context.Function("vkDestroyFence")(context.device, fence, nullptr); - if (waited != VK_SUCCESS) return destroy(); + if (bound != VK_SUCCESS) fail("vkQueueBindSparse", bound); + if (waited != VK_SUCCESS) fail("vkWaitForFences", waited); const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); - if (address == 0) return destroy(); + if (address == 0) fail("vkGetBufferDeviceAddressKHR", VK_ERROR_UNKNOWN); entry.buffer = buffer; entry.memory = VK_NULL_HANDLE; entry.address = address; @@ -564,18 +596,11 @@ bool sharedBacking(const Context& context, const HostImport& entry, BackingFile& } const char* createBackingImport(const Context& context, HostImport& entry, BackingFile& backing, VkResult& failure) { - const int file = std::exchange(backing.file, -1); bool fits = false; VkResult chunkFailure = VK_SUCCESS; - if (createChunkedDmaBufImport(context, entry, file, backing.offset, fits, chunkFailure) == nullptr && fits) { - close(file); - return nullptr; - } - if (!fits && createSparseDmaBufImport(context, entry, file, backing.offset)) { - close(file); - return nullptr; - } - return createDmaBufImport(context, entry, file, backing.offset, failure); + if (createChunkedDmaBufImport(context, entry, backing.file, backing.offset, fits, chunkFailure) == nullptr && fits) return nullptr; + if (!fits && createSparseDmaBufImport(context, entry, backing.file, backing.offset)) return nullptr; + return createDmaBufImport(context, entry, std::exchange(backing.file, -1), backing.offset, failure); } #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp new file mode 100644 index 0000000000..a6d05d4dc7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifndef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return 77; +} +#else +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint64_t Alignment = 65536; +constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; + +struct Bind { + VkDeviceSize resourceOffset; + VkDeviceSize size; + VkDeviceMemory memory; + VkDeviceSize memoryOffset; +}; + +struct MockDevice { + std::uintptr_t next = 0x1000; + std::set buffers; + std::set sparse; + std::map sizes; + std::map memories; + std::vector binds; + VkResult bindResult = VK_SUCCESS; + std::uint64_t nextAddress = 0x500000000ull; +}; + +MockDevice mock; + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { + *fence = reinterpret_cast(mock.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {} + +VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryHostPointerProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, const void*, VkMemoryHostPointerPropertiesEXT*) { + return VK_ERROR_INVALID_EXTERNAL_HANDLE; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryFdProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, int, VkMemoryFdPropertiesKHR* properties) { + properties->memoryTypeBits = 1; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = reinterpret_cast(mock.next++); + mock.buffers.insert(*buffer); + if ((info->flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0) mock.sparse.insert(*buffer); + mock.sizes[*buffer] = info->size; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + mock.buffers.erase(buffer); +} + +VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { + requirements->size = (mock.sizes.at(buffer) + Alignment - 1) / Alignment * Alignment; + requirements->alignment = Alignment; + requirements->memoryTypeBits = 1; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + *memory = reinterpret_cast(mock.next++); + mock.memories[*memory] = info->allocationSize; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + mock.memories.erase(memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockQueueBindSparse(VkQueue, std::uint32_t count, const VkBindSparseInfo* infos, VkFence) { + if (mock.bindResult != VK_SUCCESS) return mock.bindResult; + for (std::uint32_t i = 0; i < count; ++i) { + for (std::uint32_t b = 0; b < infos[i].bufferBindCount; ++b) { + const auto& buffer = infos[i].pBufferBinds[b]; + for (std::uint32_t m = 0; m < buffer.bindCount; ++m) mock.binds.push_back({buffer.pBinds[m].resourceOffset, buffer.pBinds[m].size, buffer.pBinds[m].memory, buffer.pBinds[m].memoryOffset}); + } + } + return VK_SUCCESS; +} + +VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VkBufferDeviceAddressInfo* info) { + const auto address = mock.nextAddress; + mock.nextAddress += (mock.sizes.at(info->buffer) + Alignment - 1) / Alignment * Alignment; + return address; +} + +PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + static const std::map table{ + {"vkCreateFence", reinterpret_cast(mockCreateFence)}, + {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, + {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, + {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, + {"vkGetMemoryFdPropertiesKHR", reinterpret_cast(mockGetMemoryFdProperties)}, + {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, + {"vkDestroyBuffer", reinterpret_cast(mockDestroyBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(mockGetBufferMemoryRequirements)}, + {"vkAllocateMemory", reinterpret_cast(mockAllocateMemory)}, + {"vkFreeMemory", reinterpret_cast(mockFreeMemory)}, + {"vkBindBufferMemory", reinterpret_cast(mockBindBufferMemory)}, + {"vkQueueBindSparse", reinterpret_cast(mockQueueBindSparse)}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, + }; + const auto it = table.find(name); + return it == table.end() ? nullptr : it->second; +} + +Context mockContext() { + static const auto device = reinterpret_cast(mock.next++); + static const auto queue = reinterpret_cast(mock.next++); + Context context{}; + context.device = device; + context.queue = queue; + context.deviceProc = mockProc; + context.hostImportAlignment = Alignment; + context.dmaBufImport = true; + context.sparseBinding = true; + return context; +} + +struct Backing { + int file = -1; + std::uint8_t* base = nullptr; + std::uint64_t bytes = 0; + std::uint64_t fileOffset = 0; + void* reservation = nullptr; + std::uint64_t reserved = 0; +}; + +std::vector backings; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + for (const auto* backing : backings) { + const auto begin = reinterpret_cast(backing->base); + if (address < begin || address + bytes > begin + backing->bytes) continue; + *file = dup(backing->file); + *offset = backing->fileOffset + (address - begin); + return *file >= 0; + } + return false; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +struct SharedRange { + Backing backing; + + SharedRange(std::uint64_t bytes, std::uint64_t fileOffset, bool sealed) { + backing.bytes = bytes; + backing.fileOffset = fileOffset; + backing.file = memfd_create("anyps5-sparse-import-mock", MFD_CLOEXEC | MFD_ALLOW_SEALING); + const auto fileBytes = (fileOffset + bytes + ChunkBytes - 1) / ChunkBytes * ChunkBytes; + if (backing.file < 0 || ftruncate(backing.file, static_cast(fileBytes)) != 0) throw std::runtime_error("cannot make the backing file"); + if (sealed && fcntl(backing.file, F_ADD_SEALS, F_SEAL_SHRINK) != 0) throw std::runtime_error("cannot seal the backing file"); + backing.reserved = bytes + Alignment; + backing.reservation = mmap(nullptr, backing.reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (backing.reservation == MAP_FAILED) throw std::runtime_error("cannot reserve the guest range"); + const auto aligned = (reinterpret_cast(backing.reservation) + Alignment - 1) / Alignment * Alignment; + void* mapped = mmap(reinterpret_cast(aligned), bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, backing.file, static_cast(fileOffset)); + if (mapped == MAP_FAILED) throw std::runtime_error("cannot map the backing file"); + backing.base = static_cast(mapped); + backings.push_back(&backing); + GuestAllocations::Mutation mutation; + mutation.Add(backing.base, static_cast(bytes), true, true, true); + } + + std::uint64_t Address() const { + return reinterpret_cast(backing.base); + } +}; + +std::uint64_t RangeBytes() { + return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u); +} + +void ImportsALargeRangeAsOneSparseBuffer() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 0, true); + const auto context = mockContext(); + mock.binds.clear(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import != nullptr, "a shared range larger than udmabuf's size limit was not imported"); + if (import == nullptr) return; + Expect(import->base == range.Address() && import->bytes == bytes && import->dmaBuf, "the import does not cover the whole range as a dma-buf import"); + Expect(mock.sparse.contains(import->buffer), "the range was not imported as one sparse buffer"); + const auto chunks = (bytes + ChunkBytes - 1) / ChunkBytes; + Expect(mock.binds.size() == chunks, "the sparse buffer is not bound to one chunk import per 64 MiB of the range"); + VkDeviceSize resource = 0; + std::set memories; + for (const auto& bind : mock.binds) { + Expect(bind.resourceOffset == resource && bind.memoryOffset == 0 && bind.size == std::min(ChunkBytes, bytes - resource), "a bind does not map its chunk import to the matching part of the range"); + Expect(mock.memories.contains(bind.memory) && mock.memories.at(bind.memory) == ChunkBytes, "a bind does not use a 64 MiB chunk import"); + memories.insert(bind.memory); + resource += bind.size; + } + Expect(memories.size() == chunks, "two parts of the range are bound to the same chunk import"); +} + +void UnalignedOffsetIsDecidedBeforeImporting() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 4096, true); + const auto context = mockContext(); + mock.binds.clear(); + const auto memories = mock.memories.size(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import == nullptr, "a range whose backing offset is not on the sparse block size was imported above udmabuf's size limit"); + Expect(mock.memories.size() == memories && mock.binds.empty(), "a chunk was imported or bound for a range that does not take the sparse path"); +} + +void FailureAfterTheDecisionThrows(bool sealed, VkResult bind, const char* step) { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 0, sealed); + const auto context = mockContext(); + mock.bindResult = bind; + const auto live = mock.buffers.size(); + std::string thrown; + try { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + HostImportFor(context, range.Address(), static_cast(bytes)); + } catch (const std::runtime_error& error) { + thrown = error.what(); + } + mock.bindResult = VK_SUCCESS; + Expect(thrown.find("sparse dma-buf import") != std::string::npos && thrown.find(step) != std::string::npos, std::string("a failure at ") + step + " after the sparse path was chosen did not throw (got \"" + thrown + "\")"); + Expect(mock.buffers.size() == live, std::string("the sparse buffer outlived the failure at ") + step); +} + +} + +int main() { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); + return 77; + } + close(udmabuf); + setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); + GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); + try { + ImportsALargeRangeAsOneSparseBuffer(); + UnalignedOffsetIsDecidedBeforeImporting(); + FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE"); + FailureAfterTheDecisionThrows(true, VK_ERROR_DEVICE_LOST, "vkQueueBindSparse"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (failures != 0) { + std::fprintf(stderr, "%d sparse dma-buf import checks failed\n", failures); + return 1; + } + std::printf("host import sparse dma-buf tests passed\n"); + return 0; +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index e3760335ee..6c271a9f48 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -62,7 +62,7 @@ int main() { try { const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); if (udmabuf < 0) { - std::printf("skipped, /dev/udmabuf is not accessible\n"); + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); return VulkanTestSkipped; } close(udmabuf); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4f638514c4..c64c7642af 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -52,7 +52,7 @@ - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill or the image copy. Results over a range that left the watch after they became pending (a host import that does not stay watched, made while they were pending) have no saved bytes, and storing them throws; saving those bytes or storing the results when the range leaves the watch is unimplemented. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. -- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. A range whose offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV) still takes the single udmabuf import, which udmabuf's `size_limit_mb` refuses above 64 MiB by default, and is copied; the NVIDIA proprietary driver has no memory type shared by dma-buf memory and sparse buffers and imports no dma-buf memory +- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [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. From fd2c300e46d36a6b2b6bebce7ede1d1f6c526224 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 18:16:22 +1300 Subject: [PATCH 292/353] fix(agc): drop the comments from the dissolve for a written V# over a copied read-only range --- .../libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp | 2 -- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 4 ---- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 2 -- 3 files changed, 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index b3e34156e7..2bcdbbbca0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -390,8 +390,6 @@ private: // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; - // Read-only descriptor ranges copied per build although they partially overlap the space's base - // regions (addDescriptorRegion); a written range overlapping one dissolves the space. std::vector> readsOverBase; std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index fa6fa693a6..7c254e6b92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2632,8 +2632,6 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b } return; case BaseOverlap::Partial: - // A read-only range keeps the space only while no written range of this build overlaps - // it: its copy and the base region's import would be two views of the same bytes. if (readOnly && !mirrorOverlaps(space->base, begin, end) && !WritesOverlap(begin, static_cast(end - begin))) { readsOverBase.emplace_back(begin, end); break; @@ -2703,8 +2701,6 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: } void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { - // A written range over a read-only range copied across base regions: the space dissolves, so the - // two share one merged region as they do without the space. if (space != nullptr && std::any_of(readsOverBase.begin(), readsOverBase.end(), [&](const auto& range) { return address < range.second && range.first < address + bytes; })) { Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); dissolveSpace(false); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2a8e6ca460..356fe0d4ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -532,8 +532,6 @@ void spaceReadAcrossImportsWrittenTests(const Context& context) { else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; - // A written V# in the lower import whose tail a read-only V# across both imports also binds, - // added after it and before it: both must map the shared bytes through one buffer. for (const bool writtenFirst : {false, true}) { for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); for (int build = 0; build < 2; ++build) { From c867b89a24a47644cee250c841c5e4675da2bcca Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Fri, 9 Oct 2026 22:31:56 +0200 Subject: [PATCH 293/353] feat(npwebapi2): implement sceNpWebApi2SetRequestTimeout The timeout setter takes (request id, timeout) and succeeds without checking the request, like the other request setters: no request reaches a service, so no timeout can expire. PPSA14632 imports it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpWebApi2/Export.cpp | 5 +++-- docs/dev/TechnicalDebt.md | 2 +- docs/dev/debt/libSceNpWebApi2.md | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 6bffefa394..f36474b481 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -198,8 +198,9 @@ int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { return 0; } -int APS5_VABI sceNpWebApi2SetRequestTimeout() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout) { + (void)request_id; + (void)timeout; return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9119b5f114..9bd067ad5c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -21,7 +21,7 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters ### Functional diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 148b2634e6..44b896dcc3 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -5,3 +5,4 @@ - [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline +- [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. Like the other request setters here it does not check the request id and succeeds: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire From 7505a8cf87645369f7b8dbe927e7910f638017e0 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 11 Oct 2026 11:40:22 +0200 Subject: [PATCH 294/353] docs(npwebapi2): say why sceNpWebApi2SetRequestTimeout accepts any request id shadPS4 looks the request up through its library and user contexts and returns the matching NOT_FOUND error. This library keeps no request table: CreateRequest hands out counter values and the other request calls accept any id, so the checks wait for request tracking across all of them. The entry moves to Silent stubs, as main's conventions check now requires. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpWebApi2.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 44b896dcc3..9e8e3461c6 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -1,8 +1,11 @@ # libSceNpWebApi2 technical debt +### Silent stubs + +- [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together + ### Unknown function info - [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline -- [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. Like the other request setters here it does not check the request id and succeeds: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire From 70389c09e90b926ece25c343f53c7b7158ba097e Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 11 Oct 2026 11:41:32 +0200 Subject: [PATCH 295/353] test(npwebapi2): set a timeout on a created request Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestNpWebApi2.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 3f31d7d99f..b01b7e1902 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -17,6 +17,10 @@ int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); +int APS5_VABI sceNpWebApi2CreateUserContext(int, int); +int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); +int APS5_VABI sceNpWebApi2SetRequestTimeout(std::int64_t, std::uint32_t); +int APS5_VABI sceNpWebApi2DeleteRequest(std::int64_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -43,6 +47,11 @@ int main() { const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); + const int user = sceNpWebApi2CreateUserContext(context, 1); + std::int64_t request = 0; + Require(user > 0 && sceNpWebApi2CreateRequest(user, "sessionManager", "/v1/sessions", "GET", nullptr, &request) == 0); + Require(sceNpWebApi2SetRequestTimeout(request, 30000000) == 0); + Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); From 040873f9dc8daab499d155d972296e117493389c Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Sun, 11 Oct 2026 17:02:50 +0200 Subject: [PATCH 296/353] feat(videoout): use native Wayland on Linux Build SDL's Wayland backend next to X11 and ask SDL for "wayland,x11" on Linux, so the window runs natively under a Wayland compositor instead of through XWayland. SDL loads libwayland, xkbcommon and libdecor at run time, so systems without them, or sessions without WAYLAND_DISPLAY, keep using X11. SDL_VIDEODRIVER still overrides the choice. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 2 +- CMakeLists.txt | 2 +- core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 3 +++ docs/dev/BUILD.md | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0b781895e..eca6c31edd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -244,8 +244,8 @@ jobs: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - - name: Install lavapipe and the SDL audio and XRandR development files - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev + - name: Install lavapipe and the SDL audio, XRandR and Wayland development files + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - id: mesa run: | echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbab5a8f97..6ffdcc4060 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: save: false - name: Install lavapipe for the GPU execution tests and the audio development files if: matrix.platform == 'linux' - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - name: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE diff --git a/CMakeLists.txt b/CMakeLists.txt index 8704dfdbd4..912c00dd52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,7 @@ set(SDL_TEST OFF CACHE BOOL "" FORCE) set(SDL_VIDEO ON CACHE BOOL "" FORCE) set(SDL_RENDER OFF CACHE BOOL "" FORCE) set(SDL_X11 ON CACHE BOOL "" FORCE) -set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) +set(SDL_WAYLAND ON CACHE BOOL "" FORCE) set(SDL_OPENGL OFF CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_VULKAN ON CACHE BOOL "" FORCE) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index a501eb12b7..b9454cc360 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -261,6 +261,9 @@ VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); #ifdef _WIN32 SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); +#endif +#ifdef __linux__ + SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 82b5576701..fd46f9943e 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -28,7 +28,7 @@ git submodule update --init --recursive - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. -- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. +- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. - Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. From 241ddda24626fe158ade20e639ad19cdbf3215c3 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 00:56:40 +0200 Subject: [PATCH 297/353] refactor(shader): split variant compilation into interface and emission steps compileVariant ran the layout-independent interface passes (ApplyStaticInterface, identity and dead-code removal, ShaderInfoCollector) and the layout-dependent emission (binding allocation, SPIR-V emission, the registration word-limit fallback, validation) as one step on a program it consumed. Split it into prepareInterface and emitVariant, which take the program by reference, and keep compileVariant as their composition with the same emission-failure recording, so a later change can emit another layout from a program whose interface is already prepared. No behaviour change: compileVariant still runs both steps once per variant and still moves the compiled information out of the program. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/Recompiler.cpp | 33 +++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 46f382ce18..edfb2f27df 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -450,7 +450,19 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) try { +template +auto recordEmissionFailure(std::exception_ptr* emissionFailure, Step&& step) -> decltype(step()) { + try { + return step(); + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } +} + +void prepareInterface(const RecompileRequest& request, IrProgram& program) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -462,7 +474,10 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra constexpr ShaderInfoCollector shaderInfoCollector; shaderInfoCollector.Collect(program, inputInfo); +} +CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) { + const auto inputInfo = RequestInputInfo(request); constexpr BindingAllocator bindingAllocator; bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); auto bindings = bindingAllocator.Allocate(program, request.layout, registration); @@ -548,12 +563,16 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra } #endif - return {request.layout, std::move(program).TakeCompiledInfo(), std::move(static_cast(bindings)), std::move(result)}; -} catch (const std::bad_alloc&) { - throw; -} catch (...) { - if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); - throw; + return {request.layout, {}, std::move(static_cast(bindings)), std::move(result)}; +} + +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) { + return recordEmissionFailure(emissionFailure, [&] { + prepareInterface(request, program); + auto variant = emitVariant(request, program); + variant.info = std::move(program).TakeCompiledInfo(); + return variant; + }); } struct SpecializedModule { From 558a8adfe9ac8d43d2f386d0c355c2bf5a8c8ec6 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 00:57:05 +0200 Subject: [PATCH 298/353] perf(shader): emit both registration layouts of a pixel shader from one program PrepareRegistered prepares every registered pixel shader twice: with the plan's layout, then with pushConstantSizeBytes = 0. The layout is not part of the source key, so both requests share one SourceEntry, but the first variant consumes the entry's front-end program. On a cold cache the second layout therefore ran PrepareResourceProgram again (translation, SSA, the fold and eliminator passes, SrtWalker, ResourceTracker) and the interface passes after it, although none of them reads the layout. Only decode, CFG and structurization were shared through the context (#2526). A ShaderPreparationContext now keeps the program it last emitted from, after the interface step, together with its source entry. Another layout of the same source at the same code address resets the binding allocation and emits from that program, as the registration word-limit fallback already does within one variant, and copies the compiled information out instead of moving it. A different source or code address builds a program as before. Building another program through the context releases the kept one first, and the program dies with the context, so no program outlives the registration that built it (the rule of #471). ResourcePrograms() counts the programs built through a context. Disk-cache loads and stores, the emission-failure memo and the variant order are unchanged. APS5_NO_PERF_LAYOUT_PAIR=1 restores the previous path; it is a neutral switch, so it does not change disk-cache keys. Co-Authored-By: Claude Opus 5.5 --- .../IntermediateRepresentation/IrProgram.hpp | 1 + .../src/IrProgram.cpp | 20 ++++++-- .../include/Optimization/ResourceProgram.hpp | 8 +++- core/shader/recompiler/Recompiler.cpp | 47 +++++++++++++++++-- core/shader/recompiler/ShaderDiskCache.cpp | 1 + 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index 8ffdb9c68a..8d051b4d14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -27,6 +27,7 @@ public: const IrResourcePlan& Resources() const; IrProgramMetadata& Metadata(); const IrProgramMetadata& Metadata() const; + [[nodiscard]] CompiledShaderInfo CompiledInfo() const&; CompiledShaderInfo TakeCompiledInfo() &&; IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7ad5b937ee..7d0a7a6c98 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -309,16 +309,18 @@ const IrProgramMetadata& IrProgram::Metadata() const { return metadata; } -CompiledShaderInfo IrProgram::TakeCompiledInfo() && { +namespace { + +CompiledShaderInfo compiledInfo(const IrResourcePlan& resourcePlan, std::uint32_t waveSize, ShaderInfo info, IrBindingLayout bindings) { CompiledShaderInfo result; result.stage = resourcePlan.stage; result.shaderHash = resourcePlan.shaderHash; result.waveSize = waveSize; result.userDataBase = resourcePlan.userDataBase; result.userDataCount = resourcePlan.userDataCount; - result.scratchDwords = resourcePlan.info.scratchDwords; - result.info = std::move(resourcePlan.info); - result.bindings = std::move(metadata.bindings); + result.scratchDwords = info.scratchDwords; + result.info = std::move(info); + result.bindings = std::move(bindings); for (const auto& output : result.info.outputs) { if (output.kind == StageOutputKind::Parameter && output.index < 32u) { result.paramExportMask |= 1u << output.index; @@ -327,6 +329,16 @@ CompiledShaderInfo IrProgram::TakeCompiledInfo() && { return result; } +} + +CompiledShaderInfo IrProgram::CompiledInfo() const& { + return compiledInfo(resourcePlan, waveSize, resourcePlan.info, metadata.bindings); +} + +CompiledShaderInfo IrProgram::TakeCompiledInfo() && { + return compiledInfo(resourcePlan, waveSize, std::move(resourcePlan.info), std::move(metadata.bindings)); +} + IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags) { IrValue& created = CreateValue(opcode, type); created.SetFlags(flags); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 2e4660e269..77328b8672 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -10,12 +10,19 @@ namespace ShaderRecompiler { struct PreparedControlFlow; +struct SourceEntry; class ShaderPreparationContext { public: [[nodiscard]] std::shared_ptr AcquireFrontend(const RecompileRequest& request); + [[nodiscard]] std::uint32_t ResourcePrograms() const; private: + friend IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); + friend struct PreparedInterface; std::shared_ptr frontend; + std::shared_ptr interfaceSource; + std::unique_ptr interfaceProgram; + std::uint32_t resourcePrograms = 0; }; [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request); @@ -23,7 +30,6 @@ private: [[nodiscard]] std::uint64_t FrontEndBuilds(); [[nodiscard]] std::shared_ptr GetResourcePlan(const RecompileRequest& request); -struct SourceEntry; struct ResourceCapture { std::shared_ptr plan; ResourceSnapshot snapshot; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index edfb2f27df..57e3fecdc1 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -162,6 +162,10 @@ std::uint64_t FrontEndBuilds() { return frontEndBuilds.load(std::memory_order_relaxed); } +std::uint32_t ShaderPreparationContext::ResourcePrograms() const { + return resourcePrograms; +} + IrProgram PrepareResourceProgram(const RecompileRequest& request) { return PrepareResourceProgram(request, nullptr); } @@ -169,7 +173,11 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) { frontEndBuilds.fetch_add(1, std::memory_order_relaxed); ShaderPreparationContext local; - const auto frontend = (preparation != nullptr ? *preparation : local).AcquireFrontend(request); + auto& context = preparation != nullptr ? *preparation : local; + ++context.resourcePrograms; + context.interfaceSource.reset(); + context.interfaceProgram.reset(); + const auto frontend = context.AcquireFrontend(request); const auto stageKind = toShaderStageKind(request.shader.stage); const auto inputInfo = RequestInputInfo(request); const auto& decoded = frontend->decoded; @@ -295,7 +303,7 @@ struct EmissionFailure { std::exception_ptr failure; }; -struct SourceEntry { +struct SourceEntry : std::enable_shared_from_this { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is // accepted only when its code matches word for word. Owned here because the request's span @@ -480,6 +488,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) const auto inputInfo = RequestInputInfo(request); constexpr BindingAllocator bindingAllocator; bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); + program.Metadata().bindingLayoutComplete = false; auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; @@ -944,6 +953,29 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes; } +} + +struct PreparedInterface { + static CompiledVariant Compile(SourceEntry& source, const RecompileRequest& request, ShaderPreparationContext& preparation, std::exception_ptr* emissionFailure) { + if (preparation.interfaceSource.get() != &source || preparation.interfaceProgram->Resources().shaderHash != request.shader.codeAddress) { + preparation.interfaceSource.reset(); + preparation.interfaceProgram.reset(); + auto program = source.program != nullptr ? std::move(source.program) : std::make_unique(PrepareResourceProgram(request, &preparation)); + recordEmissionFailure(emissionFailure, [&] { prepareInterface(request, *program); }); + preparation.interfaceSource = source.shared_from_this(); + preparation.interfaceProgram = std::move(program); + } + auto& program = *preparation.interfaceProgram; + return recordEmissionFailure(emissionFailure, [&] { + auto variant = emitVariant(request, program); + variant.info = program.CompiledInfo(); + return variant; + }); + } +}; + +namespace { + std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, bool& cacheHit, ShaderPreparationContext* preparation = nullptr) { for (const auto& candidate : source.variants) { if (sameLayout(candidate->layout, request.layout)) { @@ -968,11 +1000,16 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, } } if (variant == nullptr) { - auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation); - source.program.reset(); + static const bool layoutPair = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; std::exception_ptr emissionFailure; try { - variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + if (layoutPair && preparation != nullptr) { + variant = std::make_shared(PreparedInterface::Compile(source, request, *preparation, &emissionFailure)); + } else { + auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation); + source.program.reset(); + variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + } } catch (...) { if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, emissionFailure}); throw; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 8d934a1716..acaf9578c0 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1087,6 +1087,7 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_PERF_DISK_PLANS", "APS5_NO_PERF_DOM_INTERSECTION", "APS5_NO_PERF_ITERATION_SELECTION", + "APS5_NO_PERF_LAYOUT_PAIR", "APS5_NO_RESULT_MEMO", }; From 0d7b4b4bc6d3ac1f59d3bfdd2ae9e85765204220 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 00:57:06 +0200 Subject: [PATCH 299/353] test(agc): check both registration layouts against isolated compiles Add a --layout-pair mode to agc_shader_memory_tests, registered as agc_shader_layout_pair and agc_shader_layout_pair_off (the latter with APS5_NO_PERF_LAYOUT_PAIR=1), next to the frontend-pair tests. A pixel shader with a buffer load is prepared with a 128-byte push range and with none through one context, in both orders: without registration modules, with them at push offset 0 and at the fragment slot offset 128 that devices with graphics pipeline libraries use, and with a registration word limit that forces the fallback to plain modules. Each pair must build one program with reuse and two without, and each layout must match a compile of the same code made on its own source without a context: SPIR-V, binding layout, push range and push-data use, artifact fields and compiled shader information. The materialized result must also match an uncached Recompile. The context must not reuse a program translated at another code address or one built for another source at the same address, and it must release the kept program when it builds another one. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 115 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 881ba4a34e..e38d64f760 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1273,8 +1273,12 @@ if(BUILD_TESTING) add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_layout_pair COMMAND agc_shader_memory_tests --layout-pair) + add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) + set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS 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}) 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 6e412161ac..0238829f60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -18,6 +18,7 @@ #include "SpirvBackend/SpirvOptimizer.hpp" #endif #include "CacheKey.hpp" +#include "CompiledVariant.hpp" #include "BdaAbi.hpp" #include #include @@ -338,6 +339,114 @@ void verifyFrontendPair() { } } +ShaderRecompiler::RecompileRequest LayoutPairRequest(std::span code, std::span userData, bool registration, std::uint32_t wordLimit) { + using namespace ShaderRecompiler; + auto request = FrontendRequest(code, userData); + request.shader.stage = ShaderStage::Fragment; + request.context.compute.reset(); + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->wave32 = true; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.waveSize = 32u; + request.target.registrationModules = registration; + request.target.registrationWordLimit = wordLimit; + request.useCache = true; + return request; +} + +void verifyLayoutPairVariant(const ShaderRecompiler::SourceHandle& paired, const ShaderRecompiler::SourceHandle& isolated) { + const auto& left = *paired.artifact; + const auto& right = *isolated.artifact; + require(left.artifact.spirv == right.artifact.spirv, "layout pair: the SPIR-V differs from an isolated compile"); + require(left.bindings.layout == right.bindings.layout && left.bindings.layout.UsesPushData() == right.bindings.layout.UsesPushData() && left.bindings.pushConstantOffsetBytes == right.bindings.pushConstantOffsetBytes && left.bindings.pushConstantSizeBytes == right.bindings.pushConstantSizeBytes, "layout pair: the binding layout or push range differs from an isolated compile"); + const auto& a = left.artifact; + const auto& b = right.artifact; + require(a.registrationModule == b.registrationModule && a.bdaAbiVersion == b.bdaAbiVersion && a.memoryOffsetDword == b.memoryOffsetDword && a.shaderDataDwords == b.shaderDataDwords && a.imageMetadataDword == b.imageMetadataDword && a.runtimeImageCount == b.runtimeImageCount && a.runtimeImageResources == b.runtimeImageResources && a.hostSubgroupSize == b.hostSubgroupSize && a.parameterExports == b.parameterExports && a.fragmentParameters.size() == b.fragmentParameters.size(), "layout pair: the artifact differs from an isolated compile"); + const auto& x = left.info; + const auto& y = right.info; + require(x.stage == y.stage && x.shaderHash == y.shaderHash && x.waveSize == y.waveSize && x.userDataBase == y.userDataBase && x.userDataCount == y.userDataCount && x.scratchDwords == y.scratchDwords && x.paramExportMask == y.paramExportMask && x.info == y.info && x.bindings == y.bindings, "layout pair: the compiled shader information differs from an isolated compile"); +} + +void verifyLayoutPairIsolation(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle& handle, std::uint32_t separateLimit) { + using namespace ShaderRecompiler; + const std::vector isolatedCode(request.shader.code.begin(), request.shader.code.end()); + auto isolated = request; + isolated.shader.code = isolatedCode; + isolated.target.registrationWordLimit = separateLimit; + const auto reference = PrepareShader(isolated); + require(reference->source != handle.source, "layout pair: the isolated compile shared a source"); + verifyLayoutPairVariant(handle, *reference); + SrtRuntime runtime{}; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + auto uncached = request; + uncached.useCache = false; + verifyResult(Recompile(uncached), *MaterializeShader(request, *CaptureResources(request, runtime, handle), handle)); +} + +void verifyLayoutPair() { + using namespace ShaderRecompiler; + const bool reuse = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; + const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; + const auto pixelCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0u, 0xbf810000u}; + }; + const auto defaultLimit = SpirvTarget{}.registrationWordLimit; + struct Configuration { + bool registration; + std::uint32_t wordLimit; + std::uint32_t pushOffset; + }; + std::uint32_t variant = 0u; + for (const auto& [registration, wordLimit, pushOffset] : {Configuration{false, defaultLimit, 0u}, Configuration{true, defaultLimit, 0u}, Configuration{true, defaultLimit, 128u}, Configuration{true, 1u, 128u}}) { + for (const bool zeroFirst : {false, true}) { + const auto code = pixelCode(variant++); + auto pixel = LayoutPairRequest(code, buffer, registration, wordLimit); + pixel.layout.pushConstantOffsetBytes = pushOffset; + auto zero = pixel; + zero.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(zeroFirst ? zero : pixel, &preparation); + const auto second = PrepareShader(zeroFirst ? pixel : zero, &preparation); + require(first->source == second->source, "layout pair: the two layouts did not share a source"); + require(preparation.ResourcePrograms() == (reuse ? 1u : 2u), "layout pair: the second layout ignored the reuse switch"); + const auto& pixelHandle = zeroFirst ? second : first; + const auto& zeroHandle = zeroFirst ? first : second; + require(pixelHandle->artifact->bindings.layout.UsesPushData() && !zeroHandle->artifact->bindings.layout.UsesPushData(), "layout pair: the fixture does not distinguish the push-constant capacities"); + const bool module = registration && wordLimit == defaultLimit; + require(GetPreparedArtifact(*pixelHandle).registrationModule == module && GetPreparedArtifact(*zeroHandle).registrationModule == module, "layout pair: the word limit did not choose the module mode"); + verifyLayoutPairIsolation(pixel, *pixelHandle, wordLimit + 1u); + verifyLayoutPairIsolation(zero, *zeroHandle, wordLimit + 2u); + } + } + + const auto code = pixelCode(variant++); + const auto pixel = LayoutPairRequest(code, buffer, true, defaultLimit); + auto relocated = pixel; + relocated.shader.codeAddress += 0x1000u; + relocated.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(pixel, &preparation); + const auto moved = PrepareShader(relocated, &preparation); + require(first->source == moved->source, "layout pair: relocated code did not share the source"); + require(preparation.ResourcePrograms() == 2u && moved->artifact->info.shaderHash == relocated.shader.codeAddress, "layout pair: a program translated at another code address was reused"); + verifyLayoutPairIsolation(relocated, *moved, defaultLimit + 1u); + + const std::vector colorCode{0xbf800000u | variant++, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto color = LayoutPairRequest(colorCode, std::span(buffer).first(1u), true, defaultLimit); + color.shader.codeAddress = relocated.shader.codeAddress; + color.layout.pushConstantSizeBytes = 0u; + require(ResolveSource(color) != nullptr, "layout pair: the other source was not resolved"); + const auto colored = PrepareShader(color, &preparation); + require(colored->source != first->source && preparation.ResourcePrograms() == 2u, "layout pair: another source at the same code address reused the retained program"); + verifyLayoutPairIsolation(color, *colored, defaultLimit + 1u); + + const auto retained = colored->source.use_count(); + const auto otherCode = pixelCode(variant++); + static_cast(PrepareResourceProgram(LayoutPairRequest(otherCode, buffer, true, defaultLimit), &preparation)); + require(preparation.ResourcePrograms() == 3u && retained - colored->source.use_count() == (reuse ? 1 : 0), "layout pair: the context kept a program while it built another"); +} + void verifyRegisterSources() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -3548,6 +3657,11 @@ int main(int argc, char** argv) { std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--layout-pair") { + verifyLayoutPair(); + std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--bindless") { verifyBindlessTable(); std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; @@ -3562,6 +3676,7 @@ int main(int argc, char** argv) { verifyRegisterSources(); verifyEvaluatedValues(); verifyFrontendPair(); + verifyLayoutPair(); verifySignedSrtComparison(); verifyOutOfRangeConstantReads(); verifySignedMinimum(); From 010bfd76670d9d2ef33bdd3cd1bd8a0ec6354251 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:24:06 +0200 Subject: [PATCH 300/353] style(shader): name the emission-failure step's template parameter TStep docs/dev/CONVENTIONS.md asks for C#-style template parameter names (TKey, TValue). recordEmissionFailure, added with the split of variant compilation, named its callable parameter Step; rename it to TStep and write the template header the way the neighbouring recompiler sources do. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/Recompiler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 57e3fecdc1..9ae61adebf 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -458,8 +458,8 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -template -auto recordEmissionFailure(std::exception_ptr* emissionFailure, Step&& step) -> decltype(step()) { +template +auto recordEmissionFailure(std::exception_ptr* emissionFailure, TStep&& step) -> decltype(step()) { try { return step(); } catch (const std::bad_alloc&) { From 60e564a31fca05d6a86ac12421587792b925ae38 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:31:34 +0200 Subject: [PATCH 301/353] test(agc): compare every artifact field of both registration layouts The layout-pair check compared a subset of the prepared artifact: the fragment parameters only by count, and not the barycentric emulation, the runtime ABI version or the vertex interface fields. Its pixel fixture also had no interpolated inputs, so the fragment parameter list was empty on both sides. Compare every artifact field except the variant id, the fragment parameters element by element, and add a fifth configuration: registration modules at the fragment slot offset with two interpolated inputs, which must declare two fragment parameters in both layouts. With this, flipping the barycentric emulation flag, a fragment parameter's flat flag or the runtime ABI version on the reused path fails agc_shader_layout_pair, where the previous check passed all three. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0238829f60..8753001473 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -361,7 +361,12 @@ void verifyLayoutPairVariant(const ShaderRecompiler::SourceHandle& paired, const require(left.bindings.layout == right.bindings.layout && left.bindings.layout.UsesPushData() == right.bindings.layout.UsesPushData() && left.bindings.pushConstantOffsetBytes == right.bindings.pushConstantOffsetBytes && left.bindings.pushConstantSizeBytes == right.bindings.pushConstantSizeBytes, "layout pair: the binding layout or push range differs from an isolated compile"); const auto& a = left.artifact; const auto& b = right.artifact; - require(a.registrationModule == b.registrationModule && a.bdaAbiVersion == b.bdaAbiVersion && a.memoryOffsetDword == b.memoryOffsetDword && a.shaderDataDwords == b.shaderDataDwords && a.imageMetadataDword == b.imageMetadataDword && a.runtimeImageCount == b.runtimeImageCount && a.runtimeImageResources == b.runtimeImageResources && a.hostSubgroupSize == b.hostSubgroupSize && a.parameterExports == b.parameterExports && a.fragmentParameters.size() == b.fragmentParameters.size(), "layout pair: the artifact differs from an isolated compile"); + const auto sameParameter = [](const ShaderRecompiler::FragmentParameter& p, const ShaderRecompiler::FragmentParameter& q) { + return p.location == q.location && p.sourceLocation == q.sourceLocation && p.flat == q.flat && p.perVertex == q.perVertex && p.custom == q.custom && p.flatP10 == q.flatP10; + }; + require(a.vertexInputPatches == b.vertexInputPatches && a.memoryOffsetDword == b.memoryOffsetDword && a.shaderDataDwords == b.shaderDataDwords && a.imageMetadataDword == b.imageMetadataDword && a.runtimeImageCount == b.runtimeImageCount && a.runtimeImageResources == b.runtimeImageResources && a.registrationModule == b.registrationModule && a.bdaAbiVersion == b.bdaAbiVersion && a.runtimeAbiVersion == b.runtimeAbiVersion, "layout pair: the artifact's bindings or ABI differ from an isolated compile"); + require(a.vertexInputs == b.vertexInputs && a.vertexOffsetSgpr == b.vertexOffsetSgpr && a.instanceOffsetSgpr == b.instanceOffsetSgpr && a.vertexOffsetShared == b.vertexOffsetShared && a.instanceOffsetShared == b.instanceOffsetShared && a.vertexOffsetConflict == b.vertexOffsetConflict && a.instanceOffsetConflict == b.instanceOffsetConflict && a.hostSubgroupSize == b.hostSubgroupSize, "layout pair: the artifact's vertex interface differs from an isolated compile"); + require(a.parameterExports == b.parameterExports && std::ranges::equal(a.fragmentParameters, b.fragmentParameters, sameParameter) && a.barycentricEmulation.active == b.barycentricEmulation.active && a.barycentricEmulation.smooth == b.barycentricEmulation.smooth && a.barycentricEmulation.linear == b.barycentricEmulation.linear, "layout pair: the artifact's fragment interface differs from an isolated compile"); const auto& x = left.info; const auto& y = right.info; require(x.stage == y.stage && x.shaderHash == y.shaderHash && x.waveSize == y.waveSize && x.userDataBase == y.userDataBase && x.userDataCount == y.userDataCount && x.scratchDwords == y.scratchDwords && x.paramExportMask == y.paramExportMask && x.info == y.info && x.bindings == y.bindings, "layout pair: the compiled shader information differs from an isolated compile"); @@ -391,18 +396,28 @@ void verifyLayoutPair() { const auto pixelCode = [](std::uint32_t variant) { return std::vector{0xbf800000u | variant, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0u, 0xbf810000u}; }; + const auto interpolatedCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xc8100000u, 0xc8110001u, 0xc8140500u, 0xc8150501u, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0x05040500u, 0xbf810000u}; + }; const auto defaultLimit = SpirvTarget{}.registrationWordLimit; struct Configuration { bool registration; std::uint32_t wordLimit; std::uint32_t pushOffset; + bool interpolated; }; std::uint32_t variant = 0u; - for (const auto& [registration, wordLimit, pushOffset] : {Configuration{false, defaultLimit, 0u}, Configuration{true, defaultLimit, 0u}, Configuration{true, defaultLimit, 128u}, Configuration{true, 1u, 128u}}) { + for (const auto& [registration, wordLimit, pushOffset, interpolated] : {Configuration{false, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 128u, false}, Configuration{true, 1u, 128u, false}, Configuration{true, defaultLimit, 128u, true}}) { for (const bool zeroFirst : {false, true}) { - const auto code = pixelCode(variant++); + const auto code = interpolated ? interpolatedCode(variant++) : pixelCode(variant++); auto pixel = LayoutPairRequest(code, buffer, registration, wordLimit); pixel.layout.pushConstantOffsetBytes = pushOffset; + if (interpolated) { + pixel.context.pixel->interpolatorCount = 2u; + pixel.context.pixel->interpolatorSettings[1] = 1u; + pixel.context.pixel->inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.context.pixel->hasPerspectiveCenterVgpr = true; + } auto zero = pixel; zero.layout.pushConstantSizeBytes = 0u; ShaderPreparationContext preparation; @@ -415,6 +430,7 @@ void verifyLayoutPair() { require(pixelHandle->artifact->bindings.layout.UsesPushData() && !zeroHandle->artifact->bindings.layout.UsesPushData(), "layout pair: the fixture does not distinguish the push-constant capacities"); const bool module = registration && wordLimit == defaultLimit; require(GetPreparedArtifact(*pixelHandle).registrationModule == module && GetPreparedArtifact(*zeroHandle).registrationModule == module, "layout pair: the word limit did not choose the module mode"); + require(GetPreparedArtifact(*pixelHandle).fragmentParameters.size() == (interpolated ? 2u : 0u) && GetPreparedArtifact(*zeroHandle).fragmentParameters.size() == (interpolated ? 2u : 0u), "layout pair: the interpolated fixture did not declare its two parameters"); verifyLayoutPairIsolation(pixel, *pixelHandle, wordLimit + 1u); verifyLayoutPairIsolation(zero, *zeroHandle, wordLimit + 2u); } From 49de394787f2994525a247fc87904ce0fe2ed950 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:18:22 +0200 Subject: [PATCH 302/353] test(agc): compare the copied compiled information with the moved one The reused program hands out its compiled information through IrProgram::CompiledInfo() const&, while compileVariant still moves it out with TakeCompiledInfo(). The layout-pair fixtures are pixel shaders whose user-data base, scratch size and parameter export mask are all zero, so the comparison with isolated compiles could not tell whether the copy kept those fields: a copy that zeroed them still passed. Translate a compute program with a user-data base of 2 and 64 scratch dwords and require the copied and the moved information to agree on every field. Zeroing those fields in the copy now fails agc_shader_layout_pair and agc_shader_layout_pair_off. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 8753001473..95d292e5cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -461,6 +461,16 @@ void verifyLayoutPair() { const auto otherCode = pixelCode(variant++); static_cast(PrepareResourceProgram(LayoutPairRequest(otherCode, buffer, true, defaultLimit), &preparation)); require(preparation.ResourcePrograms() == 3u && retained - colored->source.use_count() == (reuse ? 1 : 0), "layout pair: the context kept a program while it built another"); + + const std::vector computeCode{0xbf800000u, 0xbf810000u}; + auto compute = FrontendRequest(computeCode, buffer); + compute.context.userDataBaseRegister = 2u; + compute.context.compute->scratchDwords = 64u; + auto program = PrepareResourceProgram(compute); + const auto copied = program.CompiledInfo(); + const auto taken = std::move(program).TakeCompiledInfo(); + require(taken.userDataBase == 2u && taken.scratchDwords == 64u, "layout pair: the compute fixture did not set a user-data base and scratch"); + require(copied.stage == taken.stage && copied.shaderHash == taken.shaderHash && copied.waveSize == taken.waveSize && copied.userDataBase == taken.userDataBase && copied.userDataCount == taken.userDataCount && copied.scratchDwords == taken.scratchDwords && copied.paramExportMask == taken.paramExportMask && copied.info == taken.info && copied.bindings == taken.bindings, "layout pair: the copied compiled information differs from the moved one"); } void verifyRegisterSources() { From 19c55dc80481970460e7aae0eba5b442c5ac5d5a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:41:54 +0200 Subject: [PATCH 303/353] fix(libkernel): drop the unwind cache's freshness check and let a failed forget throw Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Module/src/Module.cpp | 66 +++++------------ core/libs/tests/GuestUnwindModuleInfo.cpp | 70 +++++++++++-------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 61 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index b82436b90b..6e06be8deb 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -28,12 +28,9 @@ #ifdef _WIN32 namespace { struct UnwindImage { - bool filled = false; DWORD size = 0; bool hasTables = false; EhFrame::Tables tables; - std::uint32_t headerWord = 0; - std::uint32_t framesWord = 0; std::string name; }; @@ -43,17 +40,6 @@ std::unordered_map& UnwindImages() { return images; } -std::uint32_t ReadWord(std::uint64_t address) { - std::uint32_t value; - std::memcpy(&value, reinterpret_cast(address), sizeof(value)); - return value; -} - -bool UnwindImageCurrent(const UnwindImage& image, const IMAGE_NT_HEADERS64& nt) { - return image.filled && image.size == nt.OptionalHeader.SizeOfImage && - (!image.hasTables || (ReadWord(image.tables.header) == image.headerWord && ReadWord(image.tables.frames) == image.framesWord)); -} - void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleInfoForUnwind* info) { if (image.hasTables) { info->eh_frame_hdr_addr = image.tables.header; @@ -68,30 +54,25 @@ void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleI void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { const auto* base = static_cast(allocationBase); - const auto* dos = reinterpret_cast(base); - if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); const auto key = reinterpret_cast(base); { std::lock_guard lock(unwindImagesMutex); const auto found = UnwindImages().find(key); - if (found != UnwindImages().end() && UnwindImageCurrent(found->second, *nt)) { + if (found != UnwindImages().end()) { WriteUnwindInfo(found->second, base, info); return; } } + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); UnwindImage fresh; fresh.size = nt->OptionalHeader.SizeOfImage; fresh.hasTables = EhFrame::ReadEhmeta(key, *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); - if (fresh.hasTables) { - fresh.headerWord = ReadWord(fresh.tables.header); - fresh.framesWord = ReadWord(fresh.tables.frames); - } char path[4096] = {}; const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); fresh.name.assign(path, length); - fresh.filled = true; std::lock_guard lock(unwindImagesMutex); const auto& image = UnwindImages().insert_or_assign(key, std::move(fresh)).first->second; WriteUnwindInfo(image, base, info); @@ -101,13 +82,9 @@ bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { PVOID allocationBase = nullptr; if (RtlPcToFileHeader(reinterpret_cast(address), &allocationBase) == nullptr || allocationBase == nullptr) return false; const auto* base = static_cast(allocationBase); - const auto* dos = reinterpret_cast(base); - if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) return false; std::lock_guard lock(unwindImagesMutex); const auto found = UnwindImages().find(reinterpret_cast(base)); - if (found == UnwindImages().end() || !found->second.hasTables || !UnwindImageCurrent(found->second, *nt)) return false; + if (found == UnwindImages().end() || !found->second.hasTables) return false; info->st_size = sizeof(ModuleInfoForUnwind); WriteUnwindInfo(found->second, base, info); return true; @@ -115,26 +92,21 @@ bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { } extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { - try { - std::vector bases; - { - std::lock_guard lock(unwindImagesMutex); - for (const auto& image : UnwindImages()) bases.push_back(image.first); - } - std::vector unloaded; - for (const auto base : bases) { - HMODULE loaded = nullptr; - const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && - reinterpret_cast(loaded) == base; - if (!present) unloaded.push_back(base); - } - if (unloaded.empty()) return; + std::vector bases; + { std::lock_guard lock(unwindImagesMutex); - for (const auto base : unloaded) UnwindImages().erase(base); - } catch (...) { - std::lock_guard lock(unwindImagesMutex); - UnwindImages().clear(); + for (const auto& image : UnwindImages()) bases.push_back(image.first); } + std::vector unloaded; + for (const auto base : bases) { + HMODULE loaded = nullptr; + const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && + reinterpret_cast(loaded) == base; + if (!present) unloaded.push_back(base); + } + if (unloaded.empty()) return; + std::lock_guard lock(unwindImagesMutex); + for (const auto base : unloaded) UnwindImages().erase(base); } #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index 70019db3cd..d0519e368b 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -25,36 +26,52 @@ static bool Throws(std::uint64_t address) { return false; } -static void ReloadedModuleIsRead() { +struct FixtureCopy { + void* handle; + UnwindFixture* fixture; + std::uint64_t address; +}; + +static std::string FixtureCopyPath() { HMODULE linked = nullptr; Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(GetUnwindFixture()), &linked)); wchar_t original[MAX_PATH] = {}; Require(GetModuleFileNameW(linked, original, MAX_PATH) != 0); - const auto copy = std::filesystem::path(original).replace_filename("unwind_fixture_reload.dll"); + const auto copy = std::filesystem::path(original).replace_filename("unwind_fixture_copy.dll"); std::filesystem::copy_file(original, copy, std::filesystem::copy_options::overwrite_existing); - const auto path = copy.string(); + return copy.string(); +} +static FixtureCopy LoadFixtureCopy(const std::string& path) { + void* handle = dlopen_nid_postfix(path.c_str(), 2); + Require(handle != nullptr); + const auto get = reinterpret_cast(dlsym_nid_postfix(handle, "GetUnwindFixture_nid_no_patch")); + Require(get != nullptr); + auto* fixture = get(); + return {handle, fixture, reinterpret_cast(fixture)}; +} + +template +static void LookupOfEditedCopy(const std::string& path, Edit edit, bool throws) { + const auto copy = LoadFixtureCopy(path); + edit(*copy.fixture); + Require(Throws(copy.address) == throws); + Require(dlclose_nid_postfix(copy.handle) == 0); +} + +static void ReloadedModuleIsRead(const std::string& path) { for (int round = 0; round < 2; ++round) { - void* handle = dlopen_nid_postfix(path.c_str(), 2); - Require(handle != nullptr); - const auto get = reinterpret_cast(dlsym_nid_postfix(handle, "GetUnwindFixture_nid_no_patch")); - Require(get != nullptr); - auto* fixture = get(); - const auto address = reinterpret_cast(fixture); - MEMORY_BASIC_INFORMATION memory{}; - Require(VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != 0); - const auto base = reinterpret_cast(memory.AllocationBase); + const auto copy = LoadFixtureCopy(path); if (round == 0) { - fixture->frames.terminator = 12; - fixture->frames.spare[0] = 1; + copy.fixture->frames.terminator = 12; + copy.fixture->frames.spare[0] = 1; } ModuleInfoForUnwind info{}; - Require(sceKernelGetModuleInfoForUnwind(address, 0, &info) == 0); + Require(sceKernelGetModuleInfoForUnwind(copy.address, 0, &info) == 0); Require(info.eh_frame_size == (round == 0 ? 4 + 12 + 4 + 12 : 4 + 12)); - Require(dlclose_nid_postfix(handle) == 0); + Require(dlclose_nid_postfix(copy.handle) == 0); } - std::filesystem::remove(copy); } int main() { @@ -90,20 +107,15 @@ int main() { Require(std::memcmp(&again, &info, sizeof(info)) == 0); } - fixture->header.version = 2; - Require(Throws(address)); - fixture->header.version = 1; + const auto path = FixtureCopyPath(); + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.header.version = 2; }, true); for (const std::uint8_t encoding : {0x3b, 0x9b, 0x0f, 0xff}) { - fixture->header.framePointerEncoding = encoding; - Require(Throws(address)); + LookupOfEditedCopy(path, [encoding](UnwindFixture& edited) { edited.header.framePointerEncoding = encoding; }, true); } - fixture->header.framePointerEncoding = 0x1b; - fixture->frames.cieLength = 0xffffffffu; - Require(Throws(address)); - fixture->frames.cieLength = 12; - Require(!Throws(address)); - - ReloadedModuleIsRead(); + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.frames.cieLength = 0xffffffffu; }, true); + LookupOfEditedCopy(path, [](UnwindFixture&) {}, false); + ReloadedModuleIsRead(path); + std::filesystem::remove(path); ModuleInfoForUnwind host{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &host) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 3e38be7a72..51f82fffbb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,7 +30,7 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows -- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A cached entry is reused only while the image size and the `eh_frame_hdr` header word and first `eh_frame` length word still match, so a change deeper in the tables, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it From fce8aa8c7f1ce0b56c24768a419780dc0dcb6136 Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Tue, 13 Oct 2026 16:05:38 +0100 Subject: [PATCH 304/353] fix(agc): keep stores after occlusion dumps after them in a queued run A queued store run records each group's plain stores first and its occlusion counter dumps after them. A store started a new group only when it overlapped an earlier dump, so a label the title writes after its dumps was recorded before them. A title that polls that label then reads counters from before the dumps land. Any store after a dump now starts a new group. Epic Mickey: Rebrushed (PPSA15657) read stale or zero occlusion results every other frame and left visible geometry undrawn on alternate frames. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 269102624b..2f81860c0b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2043,7 +2043,7 @@ std::vector Recorder::QueuedWriteGroups(std::span> computed; for (std::size_t i = 0; i < writes.size(); ++i) { const auto& write = writes[i]; - if (!write.computed && std::any_of(computed.begin(), computed.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; })) { + if (!write.computed && !computed.empty()) { ends.push_back(i); computed.clear(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0564f2db2c..6a75034fa1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4304,11 +4304,13 @@ void queuedWriteGroupTests() { }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); const std::vector query{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x300, false}, {0x200, 0x2f8, true}, {0x208, 0x300, true}}; - Require(Recorder::QueuedWriteGroups(query) == Groups{6}, "a clear, a begin dump and an end dump per query slot are not one group"); + Require(Recorder::QueuedWriteGroups(query) == Groups{3, 6}, "the clear of the next query slot did not wait for the dumps before it"); const std::vector reused{{0x100, 0x1f8, true}, {0x100, 0x200, false}, {0x100, 0x1f8, true}}; Require(Recorder::QueuedWriteGroups(reused) == Groups{1, 3}, "a store over a computed write of its group did not start a new group"); const std::vector apart{{0x100, 0x1f8, true}, {0x1f8, 0x200, false}, {0x300, 0x310, false}}; - Require(Recorder::QueuedWriteGroups(apart) == Groups{3}, "stores beside the computed writes left their group"); + Require(Recorder::QueuedWriteGroups(apart) == Groups{1, 3}, "stores after the computed writes joined their group"); + const std::vector label{{0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x400, 0x408, false}}; + Require(Recorder::QueuedWriteGroups(label) == Groups{2, 3}, "a label after the dumps landed before them"); const std::vector stores{{0x100, 0x110, false}, {0x100, 0x110, false}}; Require(Recorder::QueuedWriteGroups(stores) == Groups{2}, "stores alone were split into groups"); std::uint64_t seed = 0x9e3779b97f4a7c15ull; From 9a1ac6a23dc6c6541cc2b5bb98f17dfd133ad0fa Mon Sep 17 00:00:00 2001 From: "john.maia" Date: Tue, 13 Oct 2026 16:16:14 +0100 Subject: [PATCH 305/353] test(agc): run the queued write group tests in ctest Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 881ba4a34e..ea1f579b0d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1003,10 +1003,11 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_resident_present COMMAND agc_driver_recorder_tests --resident-present-only) set_tests_properties(agc_resident_present PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_queued_write_groups COMMAND agc_driver_recorder_tests --queued-write-groups-only) add_test(NAME agc_slice_dirty COMMAND agc_driver_recorder_tests --slice-dirty-only) set_tests_properties(agc_slice_dirty PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 6a75034fa1..1807a56fc3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6474,6 +6474,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--queued-write-groups-only") { + queuedWriteGroupTests(); + std::cout << "queued write group tests passed\n"; + return 0; + } std::unique_ptr created; try { created = std::make_unique(); From 83787e692aa31628f826d850616defd1488bb9be Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 01:22:35 +0200 Subject: [PATCH 306/353] perf(agc): compare Windows direct imports only after a write-watch mark On Windows every direct host import leaves the write watch, and a storage image over it compares its whole surface with its guest-byte copy at every collect epoch. #4432 measured 7-8.6 s of every 10 s in those compares on a GTA V scene, with 98% of the compared surfaces unchanged. Tracker blocks that leave the watch now keep their write-watch marks as a hint. Leaving the watch gives a block a fresh hint stamp, and UntrackedHint walks a range of such blocks, at most once per collect epoch and thread, stamping the blocks whose pages a CPU store marked; it returns the newest stamp of the range. compareUntracked puts that stamp in its memo key in place of the collect epoch, so an image is compared again after a mark in its range, a driver store, a mapping change or a recorded GPU write. The byte compare still decides whether anything changed: a mark the driver makes without a CPU store costs one compare and drops nothing. Nothing else reads the hint. Collects, Watched, UnchangedSince, ChangedBlocks and NewestStamp still treat those blocks as untracked, and Linux is unchanged. agc_driver_dcc_keys_first_write refreshes an imported storage image over 50 collect epochs with no store: it compared the image 50 times before and 0 times now. A CPU store is still uploaded on the next refresh, and a store of the bytes already there costs one compare and keeps the pending results. agc_write_tracking checks the hint itself. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 86 +++++++++++++++++++ .../Graphics/include/Texture.hpp | 3 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 13 ++- .../libSceAgcDriver/tests/WriteTracking.cpp | 57 ++++++++++++ .../tests/execution/DccKeysFirstWrite.cpp | 67 +++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 7 files changed, 225 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 050ea60447..638dfe02d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -106,6 +106,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // collect stamps newer. std::uint64_t TrackerGeneration(); std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d7fca6508f..81f4fbc86a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -749,6 +749,7 @@ struct WriteTracker { // records in the same block (the title's per-job slots are 0x20 apart) must not count. std::vector cpuBlocks; WriteWatchCoverage coverage; + std::vector hintBlocks; #else static constexpr std::size_t LeafBlocks = std::size_t{1} << 16; static constexpr std::size_t LeafCount = std::size_t{1} << 15; @@ -791,6 +792,7 @@ struct WriteTracker { blocks.assign(size / WriteBlockBytes + 1, 0); cpuBlocks.assign(size / WriteBlockBytes + 1, 0); coverage.Initialize(base, size); + hintBlocks.assign(size / WriteBlockBytes + 1, 0); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); #else @@ -823,6 +825,35 @@ struct WriteTracker { #endif } +#ifdef _WIN32 + void startHints(std::uint64_t address, std::size_t bytes) { + const auto first = std::max(address, base); + const auto stop = std::min(address + bytes, base + size); + if (first >= stop) return; + bool started = false; + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { + if (!coverage.Covers(blockBegin(block), 1)) continue; + if (!started) ++generation; + started = true; + hintBlocks[block] = generation; + } + } + + bool hinted(std::uint64_t first, std::uint64_t stop) const { + if (first >= stop || first < base || stop > base + size) return false; + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { + if (hintBlocks[block] == 0 || coverage.Covers(blockBegin(block), 1)) return false; + } + return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); + } + + std::uint32_t newestHint(std::uint64_t first, std::uint64_t stop) const { + std::uint32_t newest = 0; + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintBlocks[block]); + return newest; + } +#endif + struct DriverPiece { std::uint32_t generation = 0; std::uint32_t begin = 0; @@ -1182,6 +1213,7 @@ void beforeUnwatch(std::uint64_t address, std::size_t bytes) { void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) { if (!tracker.watched) return; #ifdef _WIN32 + tracker.startHints(address, bytes); if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release); #else if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release); @@ -1441,6 +1473,60 @@ std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); } +#ifdef _WIN32 +namespace { + +thread_local ThreadCollectMemo threadHintMemo; + +bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) { + auto cursor = first; + bool marked = false; + while (cursor < stop) { + std::size_t count = tracker.pages.size(); + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; + if (count != 0 && !marked) ++tracker.generation; + marked = marked || count != 0; + for (std::size_t i = 0; i < count; ++i) tracker.hintBlocks[tracker.blockOf(reinterpret_cast(tracker.pages[i]))] = tracker.generation; + if (count < tracker.pages.size()) break; + cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; + } + if (marked) collectDirty.fetch_add(1, std::memory_order_relaxed); + return true; +} + +} +#endif + +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + const auto stop = (address + bytes + page - 1) & ~(page - 1); + const auto epoch = currentCollectEpoch(); + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.hinted(first, stop)) return 0; + const auto unwatched = unwatchSerial.load(std::memory_order_relaxed); + const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) { + return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end; + }); + if (memoized) { + collectMemoHits.fetch_add(1, std::memory_order_relaxed); + } else { + const TimedAccess timed(CounterCollect, bytes); + if (!walkHints(tracker, first, stop)) return 0; + if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched}; + } + return tracker.newestHint(first, stop); +#else + static_cast(address); + static_cast(bytes); + return 0; +#endif +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9214257308..93f60adcdf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -343,6 +343,7 @@ public: bool Refresh(); static std::uint64_t SinglePassMoves(); static std::uint64_t RefreshesProved(); + static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } @@ -503,7 +504,7 @@ private: std::uint64_t sliceLinearBytes = 0; SurfaceGeometry geometry; std::vector original; - mutable std::array comparedGuestBytes{}; + mutable std::array comparedGuestBytes{}; std::vector generationBaseline; // DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value. DccKeys uploadedKeys = DccKeys::Uncompressed; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index bfa9da5994..a14bb54445 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -90,6 +90,7 @@ std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKep std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; std::atomic depthCopyTextures{0}, depthCopyRefreshes{0}; std::atomic refreshesProved{0}; +std::atomic untrackedCompares{0}; struct StorageTraffic { std::mutex mutex; @@ -2039,6 +2040,10 @@ std::uint64_t StorageTexture::RefreshesProved() { return refreshesProved.load(std::memory_order_relaxed); } +std::uint64_t StorageTexture::UntrackedCompares() { + return untrackedCompares.load(std::memory_order_relaxed); +} + bool StorageTexture::ServesKeysAt(std::uint64_t dccAddress) const { const bool locked = GuestMemory::GpuMutex().HeldByThisThread(); const auto readFollowed = [&] { @@ -2110,12 +2115,16 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; + const auto epoch = GuestMemory::CollectEpoch(); + const bool whole = address == descriptor.baseAddress && bytes == guestBytes; + const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; + const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; } + untrackedCompares.fetch_add(1, std::memory_order_relaxed); const auto saved = std::span(original).subspan(static_cast(address - descriptor.baseAddress), bytes); if (GuestMemory::EqualsCommittedUnsynced(address, saved)) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index d3cf8a962a..6113a191e6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -275,6 +275,62 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } +void CheckUntrackedHint() { + constexpr std::size_t bytes = 4 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory), bytes, PAGE_READWRITE, 16384); + struct Cleanup { + void* memory; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } cleanup{const_cast(memory)}; + const auto base = reinterpret_cast(memory); + const auto hinted = base + Block; + constexpr std::size_t hintedBytes = 2 * Block; + for (std::size_t offset = 0; offset < bytes; offset += 4096) memory[offset] = 0x11; + BumpCollectEpoch(); + Require(CollectWrites(base, bytes) != 0 && UntrackedHint(base, bytes) == 0, "watched memory has an untracked hint"); + Unwatch(hinted, hintedBytes); + const auto stamp = NewestStamp(hinted, hintedBytes); + const auto first = UntrackedHint(hinted, hintedBytes); + Require(first != 0, "memory that left the watch has no untracked hint"); + Require(UntrackedHint(base, bytes) == 0 && UntrackedHint(hinted - 4096, 8192) == 0, "a range with watched blocks has an untracked hint"); + std::array host{}; + Require(UntrackedHint(reinterpret_cast(host.data()), host.size()) == 0, "memory outside the arena has an untracked hint"); + Require(!Watched(hinted, hintedBytes) && CollectWrites(hinted, hintedBytes) == 0 && !UnchangedSince(hinted, hintedBytes, TrackerGeneration()), "a hinted range reads as tracked"); + std::array generations{first, first}; + std::array changed{}; + Require(!ChangedBlocks(hinted, hintedBytes, generations, changed), "a hinted range uses block stamps"); + for (unsigned epoch = 0; epoch < 8; ++epoch) { + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint moved without a store"); + } + memory[Block + 3 * 16384 + 8] ^= 1; + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint was walked twice in one epoch"); + BumpCollectEpoch(); + const auto stored = UntrackedHint(hinted, hintedBytes); + Require(stored > first, "a CPU store into a hinted range did not move its hint"); + Require(UntrackedHint(hinted + Block, Block) == first, "a CPU store moved the hint of another block"); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == stored, "an untracked hint moved again without a store"); + memory[Block + 8] = memory[Block + 8]; + BumpCollectEpoch(); + const auto rewritten = UntrackedHint(hinted, hintedBytes); + Require(rewritten > stored, "a store of the bytes already there did not move the hint"); + std::thread([&] { memory[2 * Block + 5 * 4096] ^= 1; }).join(); + BumpCollectEpoch(); + const auto threaded = UntrackedHint(hinted + Block, Block); + Require(threaded > rewritten, "a store from another thread did not move the hint"); + Require(NewestStamp(hinted, hintedBytes) == stamp, "untracked hint walks advanced the block stamps"); + Unwatch(hinted, hintedBytes); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == threaded, "unwatching a hinted range again moved its hint"); + Unwatch(base, Block); + Require(UntrackedHint(base, Block) > threaded && UntrackedHint(hinted, hintedBytes) == threaded, "a range leaving the watch did not start a hint of its own"); +} + std::uint64_t ChildHostHole() { std::uintptr_t arena = 0; std::size_t arenaBytes = 0; @@ -717,6 +773,7 @@ int main() { #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); + CheckUntrackedHint(); CheckHostHoleInChild(); #endif CheckVersionRanges(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index ceb4547674..91399d1013 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -659,6 +659,69 @@ void RunImportWindow(AgcDriver::VulkanDevice& device, std::uint8_t* block) { #endif #ifdef _WIN32 +void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + using namespace AgcDriver::GuestMemory; + const auto address = AddressOf(block); + std::memset(block, 0x55, BlockBytes); + auto descriptor = TextureDescriptor(block, nullptr); + descriptor[2] = ((Side - 1u) >> 2u) | ((2 * Side - 1u) << 14u); + descriptor[6] = 0; + descriptor[7] = 0; + std::vector userData(16, 0u); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 4); + std::lock_guard gpu(GpuMutex()); + BumpCollectEpoch(); + Dispatch(device, WriteCode, userData); + const auto image = StorageTexture::FindPending(address, BlockBytes); + Require(image != nullptr && image->GuestBytes() == BlockBytes, "hinted import: the two-block image has no pending results"); + if (Watched(address, BlockBytes)) { + std::puts("the write-watched image stayed watched: its untracked hint is not tested"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + return; + } + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + for (int settle = 0; settle < 2; ++settle) { + BumpCollectEpoch(); + image->Refresh(); + } + const auto version = image->Version(); + const auto compares = StorageTexture::UntrackedCompares(); + for (int epoch = 0; epoch < 50; ++epoch) { + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == version, "hinted import: an unchanged image was uploaded again"); + } + char message[192]; + std::snprintf(message, sizeof(message), "hinted import: 50 collect epochs without a store compared the image %llu times", static_cast(StorageTexture::UntrackedCompares() - compares)); + Require(StorageTexture::UntrackedCompares() == compares, message); + block[SurfaceBytes + 0x100] = 0x33; + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != version, "hinted import: a CPU store into the imported image was missed"); + const auto stored = StorageTexture::UntrackedCompares(); + Require(stored > compares, "hinted import: the CPU store was found without a compare"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - stored <= 1, "hinted import: the stored image did not settle"); + Dispatch(device, WriteCode, userData); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: the second write left no pending results"); + BumpCollectEpoch(); + image->Refresh(); + const auto pendingVersion = image->Version(); + const auto beforeRewrite = StorageTexture::UntrackedCompares(); + auto* rewritten = reinterpret_cast(block + SurfaceBytes + 0x2000); + *rewritten = *rewritten; + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == pendingVersion, "hinted import: a store of the bytes already there uploaded the image"); + Require(StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: a store of the bytes already there did not cost exactly one compare"); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: a store of the bytes already there dropped the pending results"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: the image was compared again without a store"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + Require(Count(block, SurfaceBytes, 0xff) == Threads * 4u, "hinted import: the pending results were lost"); + Require(block[SurfaceBytes + 0x100] == 0x33, "hinted import: the write-back lost the CPU store"); +} + void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { using namespace AgcDriver::GuestMemory; const auto address = AddressOf(block); @@ -699,6 +762,7 @@ int main() { std::optional shared; std::optional window; std::optional pendingSingle; + std::optional hinted; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; block.emplace(false); @@ -742,6 +806,9 @@ int main() { #ifndef _WIN32 window.emplace(true); RunImportWindow(*device, window->Data()); +#else + hinted.emplace(true); + RunHintedImport(*device, hinted->Data()); #endif } else { std::puts("guest memory has no write watch: a write-watched surface is not tested"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6e826392d4..70e047a797 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -50,7 +50,7 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. Units without a write generation and ranges that leave the watch through a failed collect have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [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) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. From c99c89962f87848609a4ea419274a740f8f6a6ce Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 02:57:46 +0200 Subject: [PATCH 307/353] test(agc): cover per-block hints, fresh backing and unmarked stores Three ways to break the untracked hint passed every test: - hinted() without its coverage check walks, and resets, the marks of blocks that fresh backing put back under the watch; - walkHints stamping the walk's first block instead of the marked page's block hides a store from an image over another block; - compareUntracked without NewestStamp in a hinted key misses a GPU write into an import, which sets no mark and is only recorded by MarkWritten. CheckUntrackedHint now walks the whole range after a store into its second block and reads each block's hint on its own. It also recommits fresh backing over the hinted range: the range must have no hint, and a CPU store must still reach its collect. RunHintedImport hides a store's mark and records it with MarkWritten, and the image must still be uploaded at the next epoch. Each of the three changes above now fails agc_write_tracking or agc_driver_dcc_keys_first_write. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/WriteTracking.cpp | 18 ++++++++++++++++-- .../tests/execution/DccKeysFirstWrite.cpp | 10 +++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 6113a191e6..cec733ba76 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -321,14 +321,28 @@ void CheckUntrackedHint() { Require(rewritten > stored, "a store of the bytes already there did not move the hint"); std::thread([&] { memory[2 * Block + 5 * 4096] ^= 1; }).join(); BumpCollectEpoch(); - const auto threaded = UntrackedHint(hinted + Block, Block); + const auto threaded = UntrackedHint(hinted, hintedBytes); Require(threaded > rewritten, "a store from another thread did not move the hint"); + Require(UntrackedHint(hinted + Block, Block) == threaded && UntrackedHint(hinted, Block) == rewritten, "a walk stamped a store into the wrong block"); Require(NewestStamp(hinted, hintedBytes) == stamp, "untracked hint walks advanced the block stamps"); Unwatch(hinted, hintedBytes); BumpCollectEpoch(); Require(UntrackedHint(hinted, hintedBytes) == threaded, "unwatching a hinted range again moved its hint"); Unwatch(base, Block); - Require(UntrackedHint(base, Block) > threaded && UntrackedHint(hinted, hintedBytes) == threaded, "a range leaving the watch did not start a hint of its own"); + const auto own = UntrackedHint(base, Block); + Require(own > threaded && UntrackedHint(hinted, hintedBytes) == threaded, "a range leaving the watch did not start a hint of its own"); + GuestArena::GuestArenaReset_nid_postfix(const_cast(memory) + Block, hintedBytes); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory) + Block, hintedBytes, PAGE_READWRITE, 16384); + BumpCollectEpoch(); + const auto fresh = CollectWrites(hinted, hintedBytes); + Require(Watched(hinted, hintedBytes) && fresh != 0 && UnchangedSince(hinted, hintedBytes, fresh), "fresh backing of a hinted range is not watched"); + memory[Block + 8] = 0x5a; + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == 0, "watched fresh backing has an untracked hint"); + CollectWritesUncached(hinted, hintedBytes); + Require(!UnchangedSince(hinted, Block, fresh), "an untracked hint walk took a CPU store from watched memory"); + Unwatch(hinted, hintedBytes); + Require(UntrackedHint(hinted, hintedBytes) > own, "fresh backing leaving the watch did not start a new hint"); } std::uint64_t ChildHostHole() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 91399d1013..7de05243d1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -702,6 +702,14 @@ void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(stored > compares, "hinted import: the CPU store was found without a compare"); BumpCollectEpoch(); Require(image->Refresh() && StorageTexture::UntrackedCompares() - stored <= 1, "hinted import: the stored image did not settle"); + const auto settled = image->Version(); + block[SurfaceBytes + 0x200] = 0x44; + std::array marked{}; + std::size_t count = marked.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(address + SurfaceBytes), 4096, marked.data(), &count, true) && count == 1, "hinted import: the store to hide was not marked"); + MarkWritten(address + SurfaceBytes + 0x200, 1); + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != settled, "hinted import: a recorded store without a write-watch mark was missed"); Dispatch(device, WriteCode, userData); Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: the second write left no pending results"); BumpCollectEpoch(); @@ -719,7 +727,7 @@ void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); device.WaitIdle(); Require(Count(block, SurfaceBytes, 0xff) == Threads * 4u, "hinted import: the pending results were lost"); - Require(block[SurfaceBytes + 0x100] == 0x33, "hinted import: the write-back lost the CPU store"); + Require(block[SurfaceBytes + 0x100] == 0x33 && block[SurfaceBytes + 0x200] == 0x44, "hinted import: the write-back lost the CPU store"); } void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { From 71999169ae78f6449aa385ff35d67883a3eb2e22 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 13 Oct 2026 12:12:28 +0300 Subject: [PATCH 308/353] feat(agc): describe which layers of an array surface a texture view reads --- .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/src/GuestTextureResource.cpp | 7 +++++++ .../tests/GuestTextureResource.cpp | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 4ccd6a4c3b..997e9dc7e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -93,6 +93,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); +bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 8e854983e7..55ecd30e30 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -235,4 +235,11 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } +bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view) { + if (view.dimension == surface.dimension && view.depthOrLastArray == surface.depthOrLastArray) return true; + if (surface.dimension != TextureDimension::k2DArray) return false; + if (view.dimension == TextureDimension::k2D) return view.baseArray == 0; + return view.dimension == TextureDimension::k2DArray && view.depthOrLastArray <= surface.depthOrLastArray; +} + } diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index a605c6cd6a..bd5e36614f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -446,6 +446,24 @@ void RunGuestTextureResourceTests() { Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2DArray), "3D shape must never match a guest dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::kCube), "3D shape must never match a guest dimension"); + const auto layered = [](TextureDimension dimension, std::uint32_t lastLayer, std::uint32_t baseArray = 0) { + GuestTextureResource resource{}; + resource.dimension = dimension; + resource.depthOrLastArray = lastLayer; + resource.baseArray = baseArray; + return resource; + }; + const auto twoSlices = layered(TextureDimension::k2DArray, 1); + Require(ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0)), "a 2D texture must view the first layer of a 2D array color surface (slice-0 view with MIP0_DEPTH 1)"); + Require(ArrayLayersServeView(twoSlices, twoSlices), "a 2D array must view the same 2D array"); + Require(ArrayLayersServeView(layered(TextureDimension::k2DArray, 3), twoSlices), "a 2D array must view the first layers of a larger 2D array"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2DArray, 3)), "a 2D array view must not reach past the surface's layers"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0, 1)), "a 2D view of a later layer must not be served by the array's first layer"); + Require(ArrayLayersServeView(layered(TextureDimension::k2D, 0), layered(TextureDimension::k2D, 0)), "a 2D texture must view the same 2D surface"); + Require(!ArrayLayersServeView(layered(TextureDimension::k2D, 0), twoSlices), "a 2D surface must not serve a 2D array view"); + Require(!ArrayLayersServeView(layered(TextureDimension::k3D, 1), layered(TextureDimension::k2D, 0)), "a 3D surface's first slice must not serve a 2D texture"); + Require(!ArrayLayersServeView(layered(TextureDimension::kCube, 5), layered(TextureDimension::k2D, 0)), "a cube's first face must not serve a 2D texture"); + Fields streamed = base; streamed.minLodWarn = 0xabc; streamed.mipStatsCntEn = true; From faef399e8f16d88c51a8560af2997b0907672c49 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 13 Oct 2026 12:12:28 +0300 Subject: [PATCH 309/353] fix(agc): sample a slice-0 array color target as a 2D texture through its image --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- .../tests/execution/ArrayLayerTarget.cpp | 46 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index bfa9da5994..0f1b02c9ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -734,7 +734,7 @@ bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResour if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; // Same memory, same layout, same texel size: the GPU copy reinterprets the texels exactly as a // guest read through the sampled descriptor would. - return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && descriptor.dimension == from.dimension && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && descriptor.depthOrLastArray == from.depthOrLastArray && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); + return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && ArrayLayersServeView(from, descriptor) && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); } Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components) : context(context), storageSource(source) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 6c7664d7bf..2496afc6e7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -42,6 +42,7 @@ constexpr std::size_t LayerBytes = 65536; constexpr std::size_t SurfaceBytes = LayerBytes * Layers; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; @@ -59,9 +60,15 @@ constexpr std::array, 3> Triangle{{ }}; alignas(256) std::array, Layers> LoadCodes{}; +alignas(256) constexpr std::array Sample2DCode{ + 0x7e020d00, 0x060202f0, 0x100202ff, 0x3c000000, 0x7e0402ff, 0x3b800000, 0xf09c0108, 0x00610301, + 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000, +}; alignas(256) std::array PixelCode{}; alignas(256) std::array Output{}; +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -113,13 +120,13 @@ private: std::uint8_t* block = nullptr; }; -std::array ArrayDescriptor(std::uint64_t address, std::uint32_t swizzleMode) { +std::array SurfaceDescriptor(std::uint64_t address, std::uint32_t swizzleMode, std::uint32_t type, std::uint32_t lastLayer) { return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), - 0xfacu | (swizzleMode << 20u) | (Type2DArray << 28u), - Layers - 1u, + 0xfacu | (swizzleMode << 20u) | (type << 28u), + lastLayer, 0u, 0u, 0u, @@ -146,18 +153,27 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co device.WaitIdle(); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { +void Load(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { Output.fill(0xdeadbeefu); - auto& code = LoadCodes[layer]; - code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); - const auto texture = ArrayDescriptor(surface, swizzleMode); + const std::array pointSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; + std::copy(pointSampler.begin(), pointSampler.end(), userData.begin() + 12); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); Dispatch(device, code, userData); } +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { + auto& code = LoadCodes[layer]; + code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; + Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u)); +} + +void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) { + Load(device, Sample2DCode, SurfaceDescriptor(surface, swizzleMode, Type2D, 0u)); +} + void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::string& what) { for (std::uint32_t x = 0; x < Threads; ++x) { const auto expected = x < ScissorWidth ? inside : outside; @@ -273,6 +289,22 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw LoadLayer(device, surface, slice, swizzleMode); RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); + SampleSliceZero(device, texels, swizzleMode); +} + +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0, swizzleMode, false); + const auto pending = StorageTexture::FindPending(surface, LayerBytes); + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray, "the draw into slice 0 did not render into the image of its array"); + const auto plain = SurfaceDescriptor(surface, swizzleMode, Type2D, 0u); + Require(AgcDriver::Graphics::Texture::CanCopyFrom(*pending, AgcDriver::Graphics::DecodeTextureResource(plain)), "a 2D texture over slice 0 cannot view the array image's first layer"); + Sample2D(device, surface, swizzleMode); + RequireOutput(std::bit_cast(Drawn), Initial, "slice 0 sampled as a 2D texture after the draw"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); } } From 70d7497a2702921d2315fc82d25185d6bf905811 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 13 Oct 2026 12:12:28 +0300 Subject: [PATCH 310/353] docs(debt): note the array layer views of slice-0 color targets --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6e826392d4..c485236e9a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,6 +80,7 @@ - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. From 7262b10ae48d003066c4020677b3c2ee40cd0199 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Tue, 13 Oct 2026 19:08:57 +0300 Subject: [PATCH 311/353] test(agc): flush the slice-0 draw before clearing the cached textures SampleSliceZero cleared the cached textures while its draw's results were still pending, so the storage image was torn down with them, which ClearCachedTextures only expects at device teardown. On some runs the next Run then read the first pass's slice 0 after filling it with the initial value (3 of 20 on an RTX 4070, reported on the PR). It now flushes and waits first, as Run does before its own clear. --- .../prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 2496afc6e7..18dcef360c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -304,6 +304,8 @@ void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std: Require(AgcDriver::Graphics::Texture::CanCopyFrom(*pending, AgcDriver::Graphics::DecodeTextureResource(plain)), "a 2D texture over slice 0 cannot view the array image's first layer"); Sample2D(device, surface, swizzleMode); RequireOutput(std::bit_cast(Drawn), Initial, "slice 0 sampled as a 2D texture after the draw"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); AgcDriver::Graphics::ClearCachedTextures(device.Device()); } From f05086278db6d0059e392d57572d86c0df14e9a5 Mon Sep 17 00:00:00 2001 From: umang Date: Wed, 14 Oct 2026 11:35:34 +0530 Subject: [PATCH 312/353] fix(agc): save the guest bytes of units without a write generation when results land outside the write watch Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 7 +++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d650643804..c07cdb3a48 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3031,6 +3031,13 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; + std::vector unsaved(trackedLayers, false); + bool anyUnsaved = false; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + unsaved[layer] = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; + anyUnsaved = anyUnsaved || unsaved[layer]; + } + if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) captureGuestBytes(&unsaved); for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; // Results of this image now cover the alias's results borrowed into these units: this image // stores them, the alias no longer has to (its results, unchanged since the borrow, are in diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee7d8f29e6..2e99eb7436 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -49,7 +49,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. Units without a write generation and ranges that leave the watch through a failed collect have no saved bytes; storing such results throws. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From 895002149d5a7857cc3d81f30e4809e9b5be7d0f Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Tue, 13 Oct 2026 17:26:51 +0200 Subject: [PATCH 313/353] perf(shader): map cached SPIR-V from the shader cache files A cached variant kept its SPIR-V in memory for the whole run. On Linux and macOS the words are now a view into the mapped cache file, and a freshly compiled variant is written and mapped back, so the kernel can drop them. --- .../Execution/src/VulkanDevice.cpp | 2 +- .../tests/Float16Conversion.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 4 +- core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 2 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 23 +++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 48 +++---- .../tests/SpirvBranchTargets.cpp | 2 +- .../tests/VertexLayerExport.cpp | 4 +- .../tests/execution/F64FmaSubnormals.cpp | 2 +- .../tests/execution/LayeredVolumeDraw.cpp | 2 +- .../tests/execution/ZeroStrideVertexFetch.cpp | 4 +- core/shader/recompiler/Recompiler.cpp | 5 +- core/shader/recompiler/Recompiler.hpp | 27 ++-- core/shader/recompiler/ShaderDiskCache.cpp | 127 +++++++++++++++--- core/shader/recompiler/ShaderDiskCache.hpp | 5 +- .../recompiler/VertexInputSpecialization.hpp | 2 +- 16 files changed, 190 insertions(+), 71 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 45ccc24d26..9b536a4ef0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4278,7 +4278,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; - capture.spirv = shader.spirv; + capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; const auto layoutBindings = resources->LayoutBindings(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp index 4035a58490..baf3155a63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp @@ -42,7 +42,7 @@ std::vector recompile(std::span capabilities request.useCache = false; auto result = Recompile(request); require(!result.spirv.empty(), "v_cvt_f16_f32 did not recompile"); - return std::move(result.spirv); + return result.spirv.Copy(); } struct Usage { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c0d04b1dd2..8219f52b4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4727,7 +4727,7 @@ void aliasedBufferViewTests() { Require(declared == views, "the pixel shader declared the guest buffers as" + names + ", not as the expected " + std::to_string(views.size()) + " aliased variables"); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); - auto unaliased = pixel.spirv.Words(); + auto unaliased = pixel.spirv.Copy(); for (std::size_t at = 5; at < unaliased.size() && (unaliased[at] >> 16u) != 0; at += unaliased[at] >> 16u) { if (static_cast(unaliased[at] & 0xffffu) == spv::OpDecorate && unaliased[at + 2] == spv::DecorationAliased) unaliased[at + 2] = spv::DecorationRestrict; } @@ -5077,7 +5077,7 @@ void unorm10_11_11TargetTests() { const auto packed = ShaderRecompiler::Recompile(request); const auto unpacked = ShaderRecompiler::Recompile(plain); Require(packed.variantId == unpacked.variantId && packed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the packed draw did not reuse the registered artifact"); - Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && packed.spirv.Words() != unpacked.spirv.Words(), "the export packing did not specialize the module"); + Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && !(packed.spirv == unpacked.spirv), "the export packing did not specialize the module"); Require(ShaderRecompiler::Recompile(request).PipelineVariantId() == packed.PipelineVariantId(), "the packed specialization was not reused"); ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 47db4246d4..ac2fff1057 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -144,7 +144,7 @@ struct MeshDraw { std::uint32_t stagesRegister = 0x20u; }; -bool ReadsNumWorkgroups(const std::vector& words) { +bool ReadsNumWorkgroups(std::span words) { constexpr std::uint32_t OpDecorate = 71; constexpr std::uint32_t OpAccessChain = 65; constexpr std::uint32_t OpLoad = 61; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 7fad973a6c..184c778f5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -69,7 +69,7 @@ bool sameBindings(const std::vector& left, const std::vector< void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShaderArtifact& right, const char* what) { const std::string prefix = std::string(what) + ": "; - require(left.spirv.Words() == right.spirv.Words(), prefix + "SPIR-V differs"); + require(left.spirv == right.spirv, prefix + "SPIR-V differs"); require(left.bdaAbiVersion == right.bdaAbiVersion, prefix + "BDA ABI version differs"); require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs"); require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs"); @@ -595,6 +595,24 @@ void verifyStore() { require(ShaderDiskCache::Load(key, loaded), "a rewritten entry does not load"); } +void verifyPersist() { + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto persisted = ShaderDiskCache::Persist(key, variant); +#ifdef _WIN32 + require(persisted == nullptr, "an entry was persisted where the cache does not map entries"); +#else + require(persisted != nullptr, "a persisted entry was not read back"); + requireSameVariant(variant, *persisted, "persist round trip"); + const auto path = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); + require(WriteFileAtomically(path, std::vector(64, std::byte{0x5a})), "cannot replace the persisted entry"); + std::error_code error; + std::filesystem::remove(path, error); + requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); +#endif +} + struct ComputeRequest { std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; @@ -945,7 +963,7 @@ void verifyVertexTypeSpecialization() { } } require(checked == 4u, "vertex specialization lost attribute loads"); - require(artifact.spirv.Words() == original, "vertex specialization mutated the shared template"); + require(std::ranges::equal(artifact.spirv.Words(), original), "vertex specialization mutated the shared template"); } } auto invalid = artifact; @@ -1858,6 +1876,7 @@ int main(int argc, char** argv) { verifyPlanRoundTrip(); verifyPlanKey(); verifyStore(); + verifyPersist(); verifyAcrossProcesses(argv[0]); verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a52274cfa9..2a915f5c4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -750,7 +750,7 @@ void verifyBindlessTable() { const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; // A wave64 workgroup on a 32-wide host is held by one subgroup (two lanes per invocation). - const auto makeRequest = [&](const std::vector& code) { + const auto makeRequest = [&](std::span code) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 64; @@ -843,7 +843,7 @@ void verifyBindlessTable() { bool nonUniform = false; bool switched = false; }; - const auto scan = [&](const std::vector& words) { + const auto scan = [&](std::span words) { SpirvScan result; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -976,7 +976,7 @@ void verifyDescriptorPhis() { const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const auto makeRequest = [&](const std::vector& code, std::uint32_t waveSize = 32u) { + const auto makeRequest = [&](std::span code, std::uint32_t waveSize = 32u) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x21000u, code, 0, {}}; request.context.waveSize = waveSize; @@ -993,7 +993,7 @@ void verifyDescriptorPhis() { request.layout.pushConstantSizeBytes = 128; return request; }; - const auto countOps = [](const std::vector& words, std::uint32_t opcode) { + const auto countOps = [](std::span words, std::uint32_t opcode) { std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto length = words[cursor] >> 16u; @@ -1009,7 +1009,7 @@ void verifyDescriptorPhis() { const std::vector imageCode{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; const std::vector dynamicCode{0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, std::size_t images, std::size_t samplers) { + const auto compile = [&](std::span code, std::size_t images, std::size_t samplers) { auto request = makeRequest(code); const auto plan = GetResourcePlan(request); require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each"); @@ -1092,7 +1092,7 @@ void verifyDescriptorPhis() { const std::array prologue{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu}; const std::array epilogue{0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto planImages = [&](const std::vector& body) { + const auto planImages = [&](std::span body) { std::vector code(prologue.begin(), prologue.end()); code.insert(code.end(), body.begin(), body.end()); code.insert(code.end(), epilogue.begin(), epilogue.end()); @@ -1305,14 +1305,14 @@ void verifyHalfWaveReduction() { request.useCache = false; return Recompile(request).spirv; }; - const auto reductions = [](const std::vector& spirv, spv::Op opcode) { + const auto reductions = [](std::span spirv, spv::Op opcode) { std::uint32_t count = 0; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == static_cast(opcode) && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; } return count; }; - const auto halves = [](const std::vector& spirv, spv::Op opcode, std::uint32_t identity) { + const auto halves = [](std::span spirv, spv::Op opcode, std::uint32_t identity) { std::map definitions; std::vector reduces; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { @@ -1498,7 +1498,7 @@ void verifyNarrowVertexWave() { const auto prepares = [&prepared](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { return !prepared(code, subgroupSize, userDataBase).empty(); }; - const auto reductions = [](const std::vector& spirv) { + const auto reductions = [](std::span spirv) { std::uint32_t count = 0; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == spv::OpGroupNonUniformBitwiseOr && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; @@ -1559,8 +1559,8 @@ void verifyNarrowVertexWave() { require(first->source != second->source, "narrow vertex wave: the program prepared for 32 and for 64 lanes shares one translated source"); require(MatchesPreparedShader(narrowRequest, narrowHandle) && MatchesPreparedShader(wideRequest, wideHandle), "narrow vertex wave: a prepared program did not match its own request"); require(!MatchesPreparedShader(narrowRequest, wideHandle) && !MatchesPreparedShader(wideRequest, narrowHandle), "narrow vertex wave: a program prepared for one subgroup width matched a request for the other"); - narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv; - wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv; + narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv.Copy(); + wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv.Copy(); } require(narrow[0] == narrow[1] && wide[0] == wide[1], "narrow vertex wave: preparing for one subgroup width after the other gave another program than preparing it first"); require(narrow[0] != wide[0], "narrow vertex wave: s3 of a wave64 vertex program was the same on 32 and 64 lanes"); @@ -1589,7 +1589,7 @@ void verifySingleLaneVertexWave() { const auto compiled = [&vertex](std::span code, std::uint32_t subgroupSize, bool vertexSubgroups) { return Recompile(vertex(code, subgroupSize, vertexSubgroups)).spirv; }; - const auto usesSubgroups = [](const std::vector& spirv) { + const auto usesSubgroups = [](std::span spirv) { for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { const auto op = spirv[cursor] & 0xffffu; if (op == spv::OpCapability && spirv[cursor + 1u] >= spv::CapabilityGroupNonUniform && spirv[cursor + 1u] <= spv::CapabilityGroupNonUniformQuad) return true; @@ -1598,7 +1598,7 @@ void verifySingleLaneVertexWave() { } return false; }; - const auto hasConstant = [](const std::vector& spirv, std::uint32_t value) { + const auto hasConstant = [](std::span spirv, std::uint32_t value) { for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == spv::OpConstant && (spirv[cursor] >> 16u) == 4u && spirv[cursor + 3u] == value) return true; } @@ -2307,7 +2307,7 @@ void verifyComputedTexelOffsets() { }; const auto computed = program(0x100u, 0u); const auto constant = program(0xffu, 0x3fu | (1u << 8u)); - const auto recompile = [&](const std::vector& code, bool offsets) { + const auto recompile = [&](std::span code, bool offsets) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x30000u, code, 0, {}}; request.target = BufferTarget(); @@ -2327,7 +2327,7 @@ void verifyComputedTexelOffsets() { request.context.memory = memory.Regions(); return Recompile(request).spirv; }; - const auto sampleOperands = [](const std::vector& words) { + const auto sampleOperands = [](std::span words) { std::uint32_t mask = 0; bool gatherExtended = false; for (std::size_t cursor = 5; cursor < words.size();) { @@ -2412,7 +2412,7 @@ void verifyInt64AtomicCapabilities() { 0u, 0u, 0u, 0u}; const std::vector bufferAtomic{0xe1705000u, 0x80012803u, 0xbf810000u}; const std::vector imageAtomic{0xf03c0308u, 0x00020a08u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, spv::Capability added) { + const auto compile = [&](std::span code, spv::Capability added) { const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, static_cast(added)}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x58000u, code, 0, {}}; @@ -2431,7 +2431,7 @@ void verifyInt64AtomicCapabilities() { request.useCache = false; return Recompile(request).spirv; }; - const auto declares = [](const std::vector& words, spv::Capability capability) { + const auto declares = [](std::span words, spv::Capability capability) { for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; require(count != 0 && count <= words.size() - cursor, "64-bit atomics: truncated SPIR-V instruction"); @@ -2488,7 +2488,7 @@ void verifySpecializedBufferAtomicBindings() { require(tables == 0, "specialized buffer atomic: a buffer_atomic_inc through a V# the program specializes binds the address tables"); require(faults == 1 && result.bdaAbiVersion == BdaAbi::Version, "specialized buffer atomic: the program has no fault buffer for its unaligned atomic"); require(atomicElements == 1, "specialized buffer atomic: the V# element is not reported as atomic"); - const std::vector& words = result.spirv; + const auto words = result.spirv.Words(); std::uint32_t faultFunction = 0; std::size_t faultCalls = 0; bool physical = false; @@ -2672,7 +2672,7 @@ void verifyUnnormalizedSamplers() { }; const std::array capabilities{1u, static_cast(spv::CapabilityImageGatherExtended), static_cast(spv::CapabilityMinLod)}; std::uint64_t nextAddress = 0x40000u; - const auto recompile = [&](const std::vector& code, const std::array& data, std::uint64_t address = 0u, bool offsets = false) { + const auto recompile = [&](std::span code, const std::array& data, std::uint64_t address = 0u, bool offsets = false) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, address != 0u ? address : (nextAddress += 0x1000u), code, 0, {}}; request.context.waveSize = 32; @@ -2700,7 +2700,7 @@ void verifyUnnormalizedSamplers() { const auto proven = [&](const RecompileResult& result, bool expected) { return flags(result, DescriptorRole::GuestSamplers) == std::vector{expected} && flags(result, DescriptorRole::GuestImages) == std::vector{expected}; }; - const auto auditSpirv = [](const std::vector& words) { + const auto auditSpirv = [](std::span words) { std::size_t samples = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -2722,7 +2722,7 @@ void verifyUnnormalizedSamplers() { auditSpirv(accepted.spirv.Words()); const auto plain = recompile(lz, userData(normalized), 0x40000u); require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged"); - require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant"); + require(plain.variantId == accepted.variantId && plain.spirv == accepted.spirv, "unnormalized samplers: the unnormalized S# compiled another variant"); for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u, 0xf0880f08u}) { const auto result = recompile(program(mimg), userData(unnormalized)); @@ -2756,7 +2756,7 @@ void verifyUnnormalizedSamplers() { code.insert(code.end(), {0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u}); return code; }; - const auto foldedSamples = [](const std::vector& words) { + const auto foldedSamples = [](std::span words) { std::vector exact; std::vector sums; std::vector coordinates; @@ -2771,7 +2771,7 @@ void verifyUnnormalizedSamplers() { } return !coordinates.empty() && std::ranges::all_of(coordinates, [&](std::uint32_t id) { return std::ranges::count(sums, id) != 0 && std::ranges::count(exact, id) != 0; }); }; - const auto sampleMasks = [](const std::vector& words) { + const auto sampleMasks = [](std::span words) { std::vector masks; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -2831,7 +2831,7 @@ void verifyUnnormalizedSamplers() { pixel.target.subgroupSize = 64; pixel.layout.pushConstantSizeBytes = 128; pixel.useCache = false; - const auto implicitSamples = [](const std::vector& words) { + const auto implicitSamples = [](std::span words) { std::size_t samples = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp index dda775da27..3add236bdb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp @@ -41,7 +41,7 @@ std::vector recompile(std::span code) { request.useCache = false; auto result = Recompile(request); require(!result.spirv.empty(), "the shader did not recompile"); - return std::move(result.spirv); + return result.spirv.Copy(); } void requireDistinctTargets(const char* name, std::span code) { diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp index 27a7785a5a..1b8f715065 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp @@ -16,7 +16,7 @@ struct Module { std::uint32_t version = 0; }; -Module inspect(const std::vector& words) { +Module inspect(std::span words) { Module module; if (words.size() > 1) module.version = words[1]; std::vector layerIds; @@ -32,7 +32,7 @@ Module inspect(const std::vector& words) { return module; } -bool has(const std::vector& values, std::uint32_t value) { +bool has(std::span values, std::uint32_t value) { for (const auto entry : values) { if (entry == value) return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp index cb9823b3a0..b00770b08e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -135,7 +135,7 @@ std::vector Recompile(bool float64DenormPreserve) { request.target.float64DenormPreserve = float64DenormPreserve; request.layout = {0, 0, 0, 128}; request.useCache = false; - return ShaderRecompiler::Recompile(request).spirv; + return ShaderRecompiler::Recompile(request).spirv.Copy(); } bool HasDenormPreserve64(std::span words) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 8f419f2164..20ab53b94c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -137,7 +137,7 @@ private: std::byte* block = nullptr; }; -bool DeclaresLayer(const std::vector& words) { +bool DeclaresLayer(std::span words) { for (std::size_t at = 5; at < words.size();) { const auto length = words[at] >> 16u; const auto opcode = words[at] & 0xffffu; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index a308a40635..5715c39d27 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -78,8 +78,8 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); if (device.Target().registrationModules) { - if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words(); - Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Copy(); + Require(std::ranges::equal(vertexResult.spirv.Words(), registrationModule), std::string(value.what) + ": registration modules: the V# specialized the vertex module"); } const auto vertexPush = static_cast(vertexResult.pushConstants.size()); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 46f382ce18..2ce957d6f9 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -958,7 +958,10 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, emissionFailure}); throw; } - if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); + if (disk) { + if (auto persisted = ShaderDiskCache::Persist(diskKey, *variant)) variant = std::move(persisted); + else ShaderDiskCache::Store(std::move(diskKey), variant); + } } source.program.reset(); source.variants.push_back(variant); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9a22de5259..08de057b2d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -3,6 +3,7 @@ #include "RuntimeAbi.hpp" #include "PipelineSpecialization.hpp" +#include #include #include #include @@ -434,39 +435,43 @@ class SharedSpirv { public: SharedSpirv() = default; SharedSpirv(std::vector words) : words(std::make_shared>(std::move(words))) {} + SharedSpirv(std::shared_ptr owner, std::span view) : owner(std::move(owner)), view(view) {} SharedSpirv& operator=(std::vector other) { words = std::make_shared>(std::move(other)); + owner.reset(); + view = {}; return *this; } - [[nodiscard]] const std::vector& Words() const { return words ? *words : Empty(); } - operator const std::vector&() const { return Words(); } + [[nodiscard]] std::span Words() const { return words ? std::span(*words) : view; } + [[nodiscard]] std::vector Copy() const { return {Words().begin(), Words().end()}; } [[nodiscard]] std::size_t size() const { return Words().size(); } [[nodiscard]] bool empty() const { return Words().empty(); } [[nodiscard]] const std::uint32_t* data() const { return Words().data(); } [[nodiscard]] std::uint32_t* data() { return Mutable().data(); } [[nodiscard]] const std::uint32_t& operator[](std::size_t index) const { return Words()[index]; } [[nodiscard]] std::uint32_t& operator[](std::size_t index) { return Mutable()[index]; } - [[nodiscard]] std::vector::const_iterator begin() const { return Words().begin(); } - [[nodiscard]] std::vector::const_iterator end() const { return Words().end(); } + [[nodiscard]] std::span::iterator begin() const { return Words().begin(); } + [[nodiscard]] std::span::iterator end() const { return Words().end(); } [[nodiscard]] std::vector::iterator begin() { return Mutable().begin(); } [[nodiscard]] std::vector::iterator end() { return Mutable().end(); } void resize(std::size_t count) { Mutable().resize(count); } std::vector::iterator insert(std::vector::const_iterator where, std::initializer_list values) { return Mutable().insert(where, values); } - friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return left.words == right.words || left.Words() == right.Words(); } + friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } private: - static const std::vector& Empty() { - static const std::vector empty; - return empty; - } - // Copy on write: a holder whose words are shared gets its own copy before the first write. std::vector& Mutable() { - if (words == nullptr || words.use_count() != 1) words = std::make_shared>(Words()); + if (words == nullptr || words.use_count() != 1) { + words = std::make_shared>(Copy()); + owner.reset(); + view = {}; + } return *words; } std::shared_ptr> words; + std::shared_ptr owner; + std::span view; }; struct VertexInput { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 8d934a1716..36d4e68724 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -14,6 +14,12 @@ #elif defined(__APPLE__) #include #endif +#ifndef _WIN32 +#include +#include +#include +#include +#endif #include "ShaderCacheVersion.hpp" #include #include @@ -77,9 +83,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 352, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 568, "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"); @@ -158,6 +164,38 @@ struct FileHeader { std::uint64_t keyHash; std::uint64_t payloadHash; }; + +std::size_t PayloadOffset(std::size_t keyBytes) { + constexpr std::size_t alignment = 8; + return (sizeof(FileHeader) + keyBytes + alignment - 1) / alignment * alignment; +} + +#ifndef _WIN32 +class MappedFile { +public: + MappedFile(const void* data, std::size_t size) : data(data), size(size) {} + MappedFile(const MappedFile&) = delete; + MappedFile& operator=(const MappedFile&) = delete; + ~MappedFile() { munmap(const_cast(data), size); } + + [[nodiscard]] std::span Bytes() const { return {static_cast(data), size}; } + +private: + const void* data; + std::size_t size; +}; + +std::shared_ptr MapFile(const std::filesystem::path& path) { + const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) return nullptr; + struct stat status{}; + void* data = MAP_FAILED; + if (fstat(descriptor, &status) == 0 && status.st_size > 0) data = mmap(nullptr, static_cast(status.st_size), PROT_READ, MAP_PRIVATE, descriptor, 0); + close(descriptor); + if (data == MAP_FAILED) return nullptr; + return std::make_shared(data, static_cast(status.st_size)); +} +#endif static_assert(sizeof(FileHeader) == 48 && std::is_trivially_copyable_v); class Writer { @@ -220,7 +258,7 @@ private: class Reader { public: - explicit Reader(std::span bytes) : bytes(bytes) {} + explicit Reader(std::span bytes, std::shared_ptr owner = nullptr) : bytes(bytes), owner(std::move(owner)) {} [[nodiscard]] bool Ok() const { return ok; } [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } @@ -271,6 +309,18 @@ public: } } + SharedSpirv Spirv() { + const auto count = Count(sizeof(std::uint32_t)); + const auto* first = bytes.data() + position; + position += count * sizeof(std::uint32_t); + if (owner != nullptr && reinterpret_cast(first) % alignof(std::uint32_t) == 0) { + return SharedSpirv(owner, std::span(reinterpret_cast(first), count)); + } + std::vector words(count); + if (count != 0) std::memcpy(words.data(), first, count * sizeof(std::uint32_t)); + return SharedSpirv(std::move(words)); + } + void Flags(std::vector& flags) { const auto count = Count(1); flags.assign(count, false); @@ -316,6 +366,7 @@ private: } std::span bytes; + std::shared_ptr owner; std::size_t position = 0; bool ok = true; }; @@ -415,9 +466,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.runtimeAbiVersion); - std::vector words; - reader.Values(words); - result.spirv = std::move(words); + result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); @@ -1114,10 +1163,11 @@ std::string hex(std::uint64_t value) { std::vector wrapEntry(std::span key, std::span payload) { const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; - std::vector file(sizeof(header) + key.size() + payload.size()); + const auto payloadOffset = PayloadOffset(key.size()); + std::vector file(payloadOffset + payload.size()); std::memcpy(file.data(), &header, sizeof(header)); std::memcpy(file.data() + sizeof(header), key.data(), key.size()); - if (!payload.empty()) std::memcpy(file.data() + sizeof(header) + key.size(), payload.data(), payload.size()); + if (!payload.empty()) std::memcpy(file.data() + payloadOffset, payload.data(), payload.size()); return file; } @@ -1127,9 +1177,13 @@ LoadStatus unwrapEntry(std::span file, std::span body || header.payloadBytes != body - header.keyBytes) return LoadStatus::Rejected; + if (header.keyBytes > body) return LoadStatus::Rejected; + const auto payloadOffset = PayloadOffset(static_cast(header.keyBytes)); + if (payloadOffset > file.size() || header.payloadBytes != file.size() - payloadOffset) return LoadStatus::Rejected; const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); - payload = file.subspan(sizeof(header) + static_cast(header.keyBytes)); + const auto padding = file.subspan(sizeof(header) + storedKey.size(), payloadOffset - sizeof(header) - storedKey.size()); + if (std::ranges::any_of(padding, [](std::byte value) { return value != std::byte{0}; })) return LoadStatus::Rejected; + payload = file.subspan(payloadOffset); if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; @@ -1150,11 +1204,11 @@ public: [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { - return load(key, [&](std::span file) { return DecodeEntry(file, key, variant); }, hits, misses, loadFailures); + return load(key, [&](std::span file, std::shared_ptr owner) { return DecodeEntry(file, key, variant, std::move(owner)); }, hits, misses, loadFailures); } bool LoadPlan(std::span key, IrResourcePlan& plan) { - return load(key, [&](std::span file) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); + return load(key, [&](std::span file, std::shared_ptr) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); } void Store(std::vector key, std::shared_ptr variant) { @@ -1165,6 +1219,28 @@ public: enqueue({std::move(key), nullptr, std::move(plan)}); } + std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { +#ifdef _WIN32 + static_cast(key); + static_cast(variant); + return nullptr; +#else + const auto path = directory / EntryName(key); + const auto file = EncodeEntry(key, variant); + if (!WriteFileAtomically(path, file)) return nullptr; + writes.fetch_add(1, std::memory_order_relaxed); + bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); + auto mapped = MapFile(path); + if (mapped == nullptr) return nullptr; + auto persisted = std::make_shared(); + const auto bytes = mapped->Bytes(); + if (DecodeEntry(bytes, key, *persisted, std::move(mapped)) != LoadStatus::Loaded) return nullptr; + persisted->layout = variant.layout; + persisted->artifact.variantId = variant.artifact.variantId; + return persisted; +#endif + } + void Flush() { std::unique_lock lock(mutex); idle.wait(lock, [&] { return jobs.empty() && !writing; }); @@ -1200,13 +1276,23 @@ private: thread_local std::vector* fileSlot = nullptr; auto& file = ThreadOwned(fileSlot); bool loaded = false; - if (!ReadWholeFile(directory / name, file)) { + std::span bytes; + std::shared_ptr owner; +#ifndef _WIN32 + if (auto mapped = MapFile(directory / name)) { + bytes = mapped->Bytes(); + owner = std::move(mapped); + } +#endif + const bool present = owner != nullptr || ReadWholeFile(directory / name, file); + if (owner == nullptr) bytes = file; + if (!present) { missCount.fetch_add(1, std::memory_order_relaxed); } else { - switch (decode(std::span(file))) { + switch (decode(bytes, std::move(owner))) { case LoadStatus::Loaded: hitCount.fetch_add(1, std::memory_order_relaxed); - bytesRead.fetch_add(file.size(), std::memory_order_relaxed); + bytesRead.fetch_add(bytes.size(), std::memory_order_relaxed); loaded = true; break; case LoadStatus::Absent: @@ -1215,7 +1301,7 @@ private: break; case LoadStatus::Rejected: { const auto failures = failureCount.fetch_add(1, std::memory_order_relaxed); - if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); + if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), bytes.size()); break; } } @@ -1414,11 +1500,11 @@ std::vector EncodeEntry(std::span key, const Compile return wrapEntry(key, payload); } -LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant) { +LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner) { std::span payload; const auto status = unwrapEntry(file, key, payload); if (status != LoadStatus::Loaded) return status; - Reader reader(payload); + Reader reader(payload, std::move(owner)); CompiledVariant decoded; decodeArtifact(reader, decoded.artifact); decodeInfo(reader, decoded.info); @@ -1484,6 +1570,11 @@ void StorePlan(std::vector key, std::shared_ptr if (instance.Enabled() && plan != nullptr) instance.StorePlan(std::move(key), std::move(plan)); } +std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { + auto& instance = store(); + return instance.Enabled() ? instance.Persist(key, variant) : nullptr; +} + void Flush() { auto& instance = store(); if (instance.Enabled()) instance.Flush(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 530ccae9c4..327eb2f30a 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 = 30; +inline constexpr std::uint32_t FormatVersion = 31; enum class LoadStatus { Loaded, @@ -46,7 +46,7 @@ void EncodeResult(const RecompileResult& result, std::vector& out); [[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result); [[nodiscard]] std::vector EncodeEntry(std::span key, const CompiledVariant& variant); -[[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant); +[[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner = nullptr); void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key); [[nodiscard]] bool EncodePlan(const IrResourcePlan& plan, std::vector& out); @@ -59,6 +59,7 @@ void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSiz [[nodiscard]] bool Load(std::span key, CompiledVariant& variant); void Store(std::vector key, std::shared_ptr variant); +[[nodiscard]] std::shared_ptr Persist(std::span key, const CompiledVariant& variant); [[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); diff --git a/core/shader/recompiler/VertexInputSpecialization.hpp b/core/shader/recompiler/VertexInputSpecialization.hpp index 2b2722eed6..12053a78a5 100644 --- a/core/shader/recompiler/VertexInputSpecialization.hpp +++ b/core/shader/recompiler/VertexInputSpecialization.hpp @@ -16,7 +16,7 @@ namespace ShaderRecompiler { inline SharedSpirv SpecializeVertexInputTypes(const CompiledShaderArtifact& artifact, std::span classes) { if (artifact.vertexInputPatches.empty()) return artifact.spirv; - auto words = artifact.spirv.Words(); + auto words = artifact.spirv.Copy(); for (const auto& patch : artifact.vertexInputPatches) { if (patch.location >= classes.size() || classes[patch.location] >= patch.values.size() || patch.word >= words.size()) throw std::runtime_error("invalid prepared vertex type patch"); words[patch.word] = patch.values[classes[patch.location]]; From 73a808e9aa0d7a573d7cb6c87733a852506632e5 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 14 Oct 2026 12:04:18 +0530 Subject: [PATCH 314/353] perf(agc): draw stages use their shared compiled result instead of a copy Driver::draw copied every stage's compiled result (RecompileResult, with its binding, push-constant and attribute vectors) out of the shared result the capture materialized, once per stage per draw. The stages now point at the shared result. Only the CPU path of an indirect draw rewrites a stage (each record's patched user data is materialized again), so that path copies the stages it may rewrite first, on a cache hit or a miss. agc_driver_indirect_patched_stage draws through that path: a record that patches the user data word a vertex stage writes as its depth must draw with the record's value, not the register's. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Driver/Driver.hpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 26 ++- .../Execution/src/Driver/Draw/DrawCapture.cpp | 5 +- .../tests/execution/IndirectPatchedStage.cpp | 149 ++++++++++++++++++ 4 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index c305a1c933..b3aa361927 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -126,7 +126,7 @@ private: std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs); - ShaderRecompiler::RecompileResult materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); + std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming); static bool drawPrecheck(); std::optional precheckDraw(const QueueState& queue, const Submission& submission, std::span packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); 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 1ac53eebb3..dc59c267be 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -155,6 +155,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } ShaderMemory shaderMemory(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); std::vector results; + std::vector> compiledStages; + compiledStages.reserve(programs.size()); std::vector stages; results.reserve(programs.size() + 2u); stages.reserve(programs.size()); @@ -206,7 +208,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe programResults[i] = matched[i]->compiled.get(); memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); } else { - resultIndex[i] = results.size(); if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { for (auto previous = i; previous-- > 0;) { if (programResults[previous] == nullptr) continue; @@ -214,9 +215,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe break; } } - results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); + compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); if (!rejected.empty()) return DrawVerdict::Rejected; - programResults[i] = &results.back(); + programResults[i] = compiledStages.back().get(); } const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { @@ -293,17 +294,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (drawParameters.indirect && indirectCpu) { const auto indirect = *drawParameters.indirect; - if (drawHit) { - - for (std::size_t i = 0; i < programs.size(); ++i) { - if (programResults[i] == nullptr) continue; - resultIndex[i] = results.size(); - results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); - for (auto& stage : stages) { - if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; - } - programResults[i] = &results[resultIndex[i]]; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programResults[i] == nullptr) continue; + resultIndex[i] = results.size(); + results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); + for (auto& stage : stages) { + if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; } + programResults[i] = &results[resultIndex[i]]; } recordQueuedLabelsBeforeRead(submission.queue); const auto readStart = std::chrono::steady_clock::now(); @@ -345,7 +343,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index b4f9d0cd50..4f3db831ac 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -10,7 +10,7 @@ namespace AgcDriver::DriverDetail { -ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected) { +std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected) { PerformanceTimer timing("Shader.DrawStage"); using Stage = ShaderRecompiler::ShaderStage; phaseTiming.Phase(DrawRowVectors); @@ -77,9 +77,8 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st timing.Mark("capture_resources"); stageCapture.compiled = invocation.Materialize(*capture); timing.Mark("materialize"); - ShaderRecompiler::RecompileResult result = *stageCapture.compiled; phaseTiming.Phase(DrawRowRecompile); - return result; + return stageCapture.compiled; } void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp new file mode 100644 index 0000000000..7cf22540a2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t BaseVertexLocation = 0x8c; +constexpr std::uint32_t Clipped = 0x40000000u; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array PatchedInMemory{}; +alignas(256) std::array PatchedOutMemory{}; +alignas(256) std::array Records{3, 1, 0, 0, 3, 1, Clipped, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawThree() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto target = [&](std::array& memory, std::uint32_t userWord) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x76, {BaseVertexLocation, userWord}); + }; + target(DirectMemory, 0); + Append(commands, 0x2d, {3, 2}); + target(PatchedInMemory, Clipped); + Append(commands, 0x24, {0, BaseVertexLocation, 0, 2}); + target(PatchedOutMemory, 0); + Append(commands, 0x24, {16, BaseVertexLocation, 0, 2}); + return commands; +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +void PatchedStagesDrawWithTheirRecordsWords() { + DirectMemory.fill(Kept); + PatchedInMemory.fill(Kept); + PatchedOutMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawThree(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto direct = Covered(DirectMemory); + Require(direct != 0 && direct != DirectMemory.size(), "the direct draw with depth 0 covered " + std::to_string(direct) + " of " + std::to_string(DirectMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < DirectMemory.size(); ++index) { + Require(PatchedInMemory[index] == DirectMemory[index], "byte " + std::to_string(index) + " of the indirect draw whose record sets depth 0 over a clipped register value is " + std::to_string(static_cast(PatchedInMemory[index])) + ", the direct draw wrote " + std::to_string(static_cast(DirectMemory[index]))); + } + const auto out = Covered(PatchedOutMemory); + Require(out == 0, "the indirect draw whose record sets a clipped depth over a register value of 0 covered " + std::to_string(out) + " bytes, expected none"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + PatchedStagesDrawWithTheirRecordsWords(); + AgcDriverShutdown_nid_postfix(); + std::puts("CPU-path indirect draws drew with the user data word their records patched"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 8e6889666a3bfafe647c6a36a6856669f5932e8c Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 12:25:11 +0530 Subject: [PATCH 315/353] fix(libSceSsl): keep connect, read and write as NotImplemented stubs --- core/libs/prx/libSceSsl/CMakeLists.txt | 3 +- core/libs/prx/libSceSsl/Export.cpp | 31 +++++---------- core/libs/tests/GuestSsl.cpp | 54 +++----------------------- docs/dev/debt/libSceSsl.md | 2 +- 4 files changed, 16 insertions(+), 74 deletions(-) diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index a86833ac89..d66b9716ec 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -1,12 +1,11 @@ add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSsl PRIVATE libSceNet) configure_windows_unwind(libSceSsl) if(BUILD_TESTING) add_test_executable(guest_ssl_tests ${LIBS_INCLUDE_DIR}/tests/GuestSsl.cpp) target_include_directories(guest_ssl_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libSceNet libc) + target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_tests) add_test(NAME guest_ssl COMMAND guest_ssl_tests) if(WIN32) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 006534de76..f0e94b445c 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -16,8 +16,6 @@ static std::atomic g_nextHandle{1}; static std::mutex g_poolsMutex; static std::map g_pools; -extern "C" int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen); - namespace { constexpr int ERROR_NOT_FOUND = static_cast(0x8095F004); @@ -75,23 +73,6 @@ SslConnection& RequireConnection(const char* function, int sslConnectionId) { return connection->second; } -std::string Endpoint(const SslConnection& connection) { - uint8_t address[28]{}; - uint32_t length = sizeof(address); - if (sceNetGetpeername(connection.socket, address, &length) != 0 || length < 4) { - return connection.host + " (socket " + std::to_string(connection.socket) + " has no peer)"; - } - return connection.host + ":" + std::to_string((address[2] << 8) | address[3]); -} - -[[noreturn]] void ThrowNoTransport(const char* function, int sslConnectionId, const char* operation) { - SslConnection connection; - { - std::lock_guard lock(g_poolsMutex); - connection = RequireConnection(function, sslConnectionId); - } - throw std::runtime_error(std::string(function) + ": no TLS transport for " + operation + " with " + Endpoint(connection)); -} } @@ -162,21 +143,27 @@ int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbf } int APS5_VABI sceSslConnect(int sslConnectionId) { - ThrowNoTransport(__func__, sslConnectionId, "the handshake"); + (void)sslConnectionId; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { + (void)sslConnectionId; (void)buf; (void)len; (void)flags; - ThrowNoTransport(__func__, sslConnectionId, "reading"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { + (void)sslConnectionId; (void)buf; (void)len; (void)flags; - ThrowNoTransport(__func__, sslConnectionId, "writing"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceSslClose() { diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index bad0d237db..1046e7ee55 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -1,10 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include -#include #include #include #include -#include extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::size_t); @@ -20,12 +18,6 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); -int APS5_VABI sceNetSocket(const char*, int, int, int); -int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); -int APS5_VABI sceNetListen(int, int); -int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); -int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); -int APS5_VABI sceNetSocketClose(int); } struct SslMemoryPoolStats { @@ -49,16 +41,6 @@ static bool Throws(TCall call) { return false; } -template -static bool ThrowsWith(TCall call, const std::string& text) { - try { - call(); - } catch (const std::exception& error) { - return std::string(error.what()).find(text) != std::string::npos; - } - return false; -} - static int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } struct SslCaCerts { @@ -117,50 +99,24 @@ int main() { Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); - const int listener = sceNetSocket(nullptr, 2, 1, 6); - Require(listener >= 0); - std::array address{16, 2, 0, 0, 127, 0, 0, 1}; - Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0); - Require(sceNetListen(listener, 1) == 0); - std::uint32_t addressSize = address.size(); - Require(sceNetGetsockname(listener, address.data(), &addressSize) == 0); - const std::string endpoint = "example.com:" + std::to_string((address[2] << 8) | address[3]); - const int client = sceNetSocket(nullptr, 2, 1, 6); - Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); - const int idle = sceNetSocket(nullptr, 2, 1, 6); - Require(idle >= 0); - - const int connection = sceSslCreateConnection(context, client, "example.com"); + const int connection = sceSslCreateConnection(context, 0, "example.com"); Require(connection > 0 && connection != context); Require(sceSslSetVerifyCallback(connection, VerifyCallback, &marker) == 0); Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0); char buffer[4]{}; - Require(ThrowsWith([&] { sceSslConnect(connection); }, endpoint)); - Require(ThrowsWith([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); }, endpoint)); - Require(ThrowsWith([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); }, endpoint)); - - const int unconnected = sceSslCreateConnection(context, idle, "example.com"); - Require(unconnected > 0 && unconnected != connection); - Require(ThrowsWith([&] { sceSslConnect(unconnected); }, "example.com (socket")); - Require(sceSslDeleteConnection(unconnected) == 0); + Require(Throws([&] { sceSslConnect(connection); })); + Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); + Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); const int unknown = connection + 1000; Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); - Require(ThrowsWith([&] { sceSslConnect(unknown); }, "unknown connection")); - Require(ThrowsWith([&] { sceSslRead(unknown, buffer, sizeof(buffer), 0); }, "unknown connection")); - Require(ThrowsWith([&] { sceSslWrite(unknown, buffer, sizeof(buffer), 0); }, "unknown connection")); Require(Throws([&] { sceSslDeleteConnection(unknown); })); - Require(Throws([&] { sceSslConnect(unconnected); })); Require(sceSslTerm_nid_postfix(context) == 0); Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); - Require(ThrowsWith([&] { sceSslConnect(connection); }, "terminated")); - Require(sceSslCreateConnection(context, client, "example.com") == invalidId); + Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); Require(sceSslDeleteConnection(connection) == 0); Require(Throws([&] { sceSslDeleteConnection(connection); })); - Require(sceNetSocketClose(idle) == 0); - Require(sceNetSocketClose(client) == 0); - Require(sceNetSocketClose(listener) == 0); Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 70045e2483..a789cb5729 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw with the host and the socket's peer port where the handshake would start. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetCaList, sceSslFreeCaList, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and the 16-byte `SceSslCaList` layout (an array of certificate handles and a 32-bit count) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs) and shadPS4's `OrbisSslCaList`, not confirmed for the PS5. On the console the list holds the certificate authorities the SSL service has; [shadPS4](https://github.com/shadps4-emu/shadPS4/pull/4104) returns a placeholder certificate from `sceSslGetCaCerts` because Rise of the Tomb Raider fails its initialization checks without one. No root certificate bundle is shipped and no loaded certificate is kept, so here a context holds none: `sceSslGetCaList` clears the list and returns `NOT_FOUND`, as `sceSslGetCaCerts` does, `sceSslFreeCaList` accepts only that cleared list, and `sceSslUnloadCert` (imported by PPSA15508) has nothing to drop and is assumed to return 0. A null list returns `0x8095177A`, which `sceSslGetCaCerts` and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/libs/network.cpp) return for a null argument (shadPS4, PS4, returns `0x8095F007`). An unknown context throws (KytyPS5 returns `SSL_ERROR_INVALID_ID`, `0x8095F006`, from `sceSslGetCaCerts`), and so does a list `sceSslGetCaList` did not return From 76c0ba53e33e83ff7e90de4651293f2f728b3439 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 14 Oct 2026 02:02:38 -0400 Subject: [PATCH 316/353] feat(agc): present BT.2100 PQ display buffers on an SDR output Accept the 10-bit BT.2100 PQ display formats 0x8100070422000000 (red in the low bits) and 0x8100070400000000 (blue in the low bits) and convert them to 8-bit sRGB when presented: PQ EOTF, BT.2020 to BT.709 primaries, a luminance tone map whose white level is the tone-map luminance sceSystemServiceGetHdrToneMapLuminance reports (reference white min(203 nits, white level)), and the sRGB encoding. The conversion runs on the CPU for linear buffers and DCC clear values, in the color transfer shader for tiled guest buffers, and in the presentation pass for resident images. The three share the PQ EOTF table computed once in double precision. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../Execution/include/DisplayFormat.hpp | 1 + .../Execution/include/PqToSdr.hpp | 78 +++++++++++++++++ .../Execution/include/PresentationPass.hpp | 11 ++- .../Execution/include/ToneMapLuminance.hpp | 10 +++ .../Execution/src/DisplayBuffer.cpp | 19 +++-- .../Execution/src/DisplayFormat.cpp | 8 +- .../Execution/src/PresentationPass.cpp | 46 +++++++--- .../Execution/src/VulkanDevice.cpp | 6 +- .../Graphics/include/GpuColorTransfer.hpp | 8 +- .../Graphics/shaders/ColorTransfer.comp | 12 +++ .../Graphics/shaders/PqToSdr.glsl | 28 ++++++ .../Graphics/shaders/PresentationPass.frag | 9 ++ .../Graphics/src/GpuColorTransfer.cpp | 46 ++++++---- .../prx/libSceAgcDriver/PostCompile.cmake | 2 +- .../tests/DisplayBufferDcc.cpp | 8 ++ .../tests/PresentationPass.cpp | 66 ++++++++++++-- .../libSceAgcDriver/tests/ResidentPresent.cpp | 48 +++++++++-- core/libs/prx/libSceSystemService/Export.cpp | 6 +- core/libs/tests/LinearDisplayBuffer.cpp | 85 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 21 files changed, 437 insertions(+), 63 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ea1f579b0d..c49580a1cd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -120,7 +120,7 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) COMMAND "$" -V --target-env vulkan1.1 -o "${agcPresentationShaderSpv}" "${agcPresentationShaderSource}" COMMAND ${CMAKE_COMMAND} -DINPUT=${agcPresentationShaderSpv} -DOUTPUT=${agcPresentationShaderHeader} -DSYMBOL=PRESENTATION_PASS_${agcPresentationSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" - DEPENDS "${agcPresentationShaderSource}" glslang-standalone + DEPENDS "${agcPresentationShaderSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone VERBATIM ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp index 63e5d7749c..6ea0e234ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -12,6 +12,7 @@ VkFormat DisplayTexelFormat(std::uint64_t pixelFormat); bool DisplayTenBit(std::uint64_t pixelFormat); bool DisplayRedLow(std::uint64_t pixelFormat); bool DisplayHalfFloat(std::uint64_t pixelFormat); +bool DisplayPq(std::uint64_t pixelFormat); std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat); enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp new file mode 100644 index 0000000000..eca1e61a9e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp @@ -0,0 +1,78 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP + +#include +#include +#include +#include +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" + +namespace AgcDriver { + +inline constexpr float PqReferenceWhiteNits = 203.0f; +inline constexpr float PqToneMapKnee = 0.75f; +inline constexpr std::uint32_t PqCodes = 1024; + +inline const std::array& PqEotfNits() { + static const auto table = [] { + std::array 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; + for (std::uint32_t code = 0; code < PqCodes; ++code) { + const double e = std::pow(code / 1023.0, 1.0 / m2); + nits[code] = static_cast(10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1)); + } + return nits; + }(); + return table; +} + +inline float PqReferenceWhite(float whiteNits) { + return std::min(PqReferenceWhiteNits, whiteNits); +} + +inline float PqToneMap(float luminance, float relativeWhite) { + if (luminance <= PqToneMapKnee) return luminance; + const float t = (luminance - PqToneMapKnee) / (1.0f - PqToneMapKnee); + const float w = (relativeWhite - PqToneMapKnee) / (1.0f - PqToneMapKnee); + return PqToneMapKnee + (1.0f - PqToneMapKnee) * t * (1.0f + t / (w * w)) / (1.0f + t); +} + +inline std::array PqToSdrLinear(float red, float green, float blue, float whiteNits) { + const float referenceWhite = PqReferenceWhite(whiteNits); + const float r2020 = red / referenceWhite; + const float g2020 = green / referenceWhite; + const float b2020 = blue / referenceWhite; + std::array rgb{ + std::max(1.6604910f * r2020 - 0.5876411f * g2020 - 0.0728499f * b2020, 0.0f), + std::max(-0.1245505f * r2020 + 1.1328999f * g2020 - 0.0083494f * b2020, 0.0f), + std::max(-0.0181508f * r2020 - 0.1005789f * g2020 + 1.1187297f * b2020, 0.0f)}; + const float luminance = 0.2126f * rgb[0] + 0.7152f * rgb[1] + 0.0722f * rgb[2]; + if (luminance > 0.0f) { + const float scale = PqToneMap(luminance, whiteNits / referenceWhite) / luminance; + for (auto& channel : rgb) channel = std::min(channel * scale, 1.0f); + } + return rgb; +} + +inline std::uint8_t SrgbEncode8(float linear) { + const float v = std::clamp(linear, 0.0f, 1.0f); + const float encoded = v <= 0.0031308f ? 12.92f * v : 1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f; + return static_cast(std::floor(encoded * 255.0f + 0.5f)); +} + +inline std::array PqToSdrPixel(std::uint32_t word, bool redLow, float whiteNits) { + const auto& nits = PqEotfNits(); + const float low = nits[word & 0x3ffu]; + const float green = nits[(word >> 10u) & 0x3ffu]; + const float high = nits[(word >> 20u) & 0x3ffu]; + const auto rgb = PqToSdrLinear(redLow ? low : high, green, redLow ? high : low, whiteNits); + return {SrgbEncode8(rgb[2]), SrgbEncode8(rgb[1]), SrgbEncode8(rgb[0]), 0xff}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp index 00e262b03f..3bcf7f2085 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -6,11 +6,16 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include +#include #include #include namespace AgcDriver { +namespace Graphics { +class Buffer; +} + class PresentationPass { public: PresentationPass(const Graphics::Context& context, std::size_t slots); @@ -18,7 +23,7 @@ public: PresentationPass(const PresentationPass&) = delete; PresentationPass& operator=(const PresentationPass&) = delete; - void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, std::uint32_t layer = 0); + void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer = 0); void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout); void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); void Release(std::size_t slot) noexcept; @@ -36,6 +41,7 @@ private: std::uint32_t width = 0; std::uint32_t height = 0; std::uint32_t mode = 0; + float whiteNits = 0.0f; std::vector> transient; VkImage dumpImage = VK_NULL_HANDLE; VkDeviceMemory dumpMemory = VK_NULL_HANDLE; @@ -49,7 +55,7 @@ private: VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; void ensureDumpTarget(Slot& slot, VkExtent2D extent); void releaseDumpTarget(Slot& slot) noexcept; - void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[8]) const; + void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const; void release() noexcept; Graphics::Context context; @@ -59,6 +65,7 @@ private: VkDescriptorPool descriptorPool = VK_NULL_HANDLE; VkShaderModule vertex = VK_NULL_HANDLE; VkShaderModule fragment = VK_NULL_HANDLE; + std::unique_ptr pqNits; std::map, Target> targets; std::vector slots; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp new file mode 100644 index 0000000000..b00601533e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP + +namespace AgcDriver { + +inline constexpr float SdrToneMapLuminanceNits = 100.0f; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index d59fe232b6..de10849034 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include @@ -58,7 +59,14 @@ void decodeHalfFloatTexel(const std::byte* texel, std::byte* pixel, bool redLow) pixel[3] = static_cast(Graphics::HalfFloatScanoutCode(halves[3], true)); } -void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit) { +void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit, bool pq) { + if (pq) { + std::uint32_t word = 0; + std::memcpy(&word, texel, 4); + const auto converted = PqToSdrPixel(word, rgba, SdrToneMapLuminanceNits); + for (std::size_t i = 0; i < converted.size(); ++i) pixel[i] = static_cast(converted[i]); + return; + } if (tenBit) { std::uint32_t word = 0; std::memcpy(&word, texel, 4); @@ -116,7 +124,7 @@ void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { require(buffer.width != 0 && buffer.height != 0 && buffer.width <= 16384 && buffer.height <= 16384, "VideoOut: invalid display buffer dimensions"); const bool halfFloat = DisplayHalfFloat(buffer.pixelFormat); - if (!halfFloat && baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { + if (!halfFloat && !DisplayPq(buffer.pixelFormat) && baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { char message[96]; std::snprintf(message, sizeof(message), "VideoOut: unsupported display pixel format 0x%016llx", static_cast(buffer.pixelFormat)); throw std::runtime_error(message); @@ -157,7 +165,8 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa return pixels; } const auto blocksPerRow = (buffer.width + 127u) / 128u; - const bool rgba = baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8; + const bool rgba = DisplayRedLow(buffer.pixelFormat); + const bool pq = DisplayPq(buffer.pixelFormat); // Bit 56 marks the 10-bit variant: A2B10G10R10 with red in the low bits (the scanout draws render // COLOR_2_10_10_10 into it). Presented as 8-bit BGRA by dropping the low two bits of each channel. const bool tenBit = (buffer.pixelFormat & PixelFormatUnormBit) != 0; @@ -168,7 +177,7 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; - decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit); + decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit, pq); } } timing.Mark("detile_convert"); @@ -193,7 +202,7 @@ std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr throw std::runtime_error(message); } std::array pixel{}; - decodeTexel(texel.data(), pixel.data(), baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8, (buffer.pixelFormat & PixelFormatUnormBit) != 0); + decodeTexel(texel.data(), pixel.data(), DisplayRedLow(buffer.pixelFormat) && !DisplayHalfFloat(buffer.pixelFormat), (buffer.pixelFormat & PixelFormatUnormBit) != 0, DisplayPq(buffer.pixelFormat)); return pixel; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp index d6447e9048..34c6c08530 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -7,6 +7,8 @@ constexpr std::uint64_t PixelFormatUnormBit = 0x0100000000000000ull; constexpr std::uint64_t PixelFormatR8G8B8A8 = 0x8000000022000000ull; constexpr std::uint64_t PixelFormatR16G16B16A16Float = 0xc001000622000000ull; constexpr std::uint64_t PixelFormatB16G16R16A16Float = 0xc001000600000000ull; +constexpr std::uint64_t PixelFormatR10G10B10A2Bt2100Pq = 0x8100070422000000ull; +constexpr std::uint64_t PixelFormatB10G10R10A2Bt2100Pq = 0x8100070400000000ull; } @@ -15,7 +17,11 @@ bool DisplayTenBit(std::uint64_t pixelFormat) { } bool DisplayRedLow(std::uint64_t pixelFormat) { - return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8 || pixelFormat == PixelFormatR16G16B16A16Float; + return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8 || pixelFormat == PixelFormatR16G16B16A16Float || pixelFormat == PixelFormatR10G10B10A2Bt2100Pq; +} + +bool DisplayPq(std::uint64_t pixelFormat) { + return pixelFormat == PixelFormatR10G10B10A2Bt2100Pq || pixelFormat == PixelFormatB10G10R10A2Bt2100Pq; } bool DisplayHalfFloat(std::uint64_t pixelFormat) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ee805692a0..ddb45a05f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -1,9 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassFrag_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" +#include +#include +#include +#include namespace AgcDriver { namespace { @@ -12,6 +17,7 @@ constexpr std::uint32_t ModeTenBit = 1; constexpr std::uint32_t ModeRedLow = 2; constexpr std::uint32_t ModeKeepTenBit = 4; constexpr std::uint32_t ModeSubsample = 8; +constexpr std::uint32_t ModePq = 16; } @@ -26,23 +32,23 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; Graphics::Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler presentation pass"); - const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &sampler}; + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &sampler}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}}}; VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - descriptorInfo.bindingCount = 1; - descriptorInfo.pBindings = &binding; + descriptorInfo.bindingCount = static_cast(bindings.size()); + descriptorInfo.pBindings = bindings.data(); Graphics::Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout presentation pass"); - const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0, 32}; + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0, 36}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; layoutInfo.setLayoutCount = 1; layoutInfo.pSetLayouts = &descriptorLayout; layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout presentation pass"); - const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(slotCount)}; + const std::array sizes{{{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(slotCount)}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(slotCount)}}}; VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.maxSets = static_cast(slotCount); - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &size; + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); Graphics::Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool presentation pass"); for (auto& slot : slots) { VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; @@ -51,6 +57,19 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t allocation.pSetLayouts = &descriptorLayout; Graphics::Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &slot.set), "vkAllocateDescriptorSets presentation pass"); } + const auto& luminance = PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const VkDescriptorBufferInfo tableInfo{pqNits->Handle(), 0, sizeof(luminance)}; + std::vector tableWrites(slots.size(), VkWriteDescriptorSet{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}); + for (std::size_t i = 0; i < slots.size(); ++i) { + tableWrites[i].dstSet = slots[i].set; + tableWrites[i].dstBinding = 1; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + tableWrites[i].pBufferInfo = &tableInfo; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); const auto createModule = context.Function("vkCreateShaderModule"); VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); @@ -85,6 +104,7 @@ void PresentationPass::release() noexcept { if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); if (sampler) context.Function("vkDestroySampler")(context.device, sampler, nullptr); + pqNits.reset(); fragment = vertex = VK_NULL_HANDLE; descriptorPool = VK_NULL_HANDLE; pipelineLayout = VK_NULL_HANDLE; @@ -226,16 +246,18 @@ const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImag return targets.emplace(key, created).first->second; } -void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, std::uint32_t layer) { +void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer) { Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation pass source is unavailable"); Graphics::Require(!DisplayHalfFloat(pixelFormat), "presentation pass does not decode 16-bit float display buffers"); + Graphics::Require(std::isfinite(whiteNits) && whiteNits >= 1.0f, "presentation pass white level must be at least 1 nit"); Release(index); auto& slot = slots[index]; slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT, layer); slot.width = width; slot.height = height; - slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u); + slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u) | (DisplayPq(pixelFormat) ? ModePq : 0u); + slot.whiteNits = whiteNits; const VkDescriptorImageInfo imageInfo{sampler, slot.source, VK_IMAGE_LAYOUT_GENERAL}; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = slot.set; @@ -246,7 +268,7 @@ void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t widt context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); } -void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[8]) const { +void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const { VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; begin.renderPass = entry.renderPass; begin.framebuffer = framebuffer; @@ -279,7 +301,7 @@ void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index slot.transient.emplace_back(view, framebuffer); const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); const std::uint32_t mode = slot.mode | (TenBitFormat(format) ? ModeKeepTenBit : 0u); - const std::int32_t parameters[8] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode)}; + const std::int32_t parameters[9] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode), std::bit_cast(slot.whiteNits)}; draw(commands, slot, entry, framebuffer, extent, parameters); } @@ -334,7 +356,7 @@ void PresentationPass::RecordDump(VkCommandBuffer commands, std::size_t index, s const auto extent = DumpExtent(slot.width, slot.height, stride); ensureDumpTarget(slot, extent); const auto& entry = target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - const std::int32_t parameters[8] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample)}; + const std::int32_t parameters[9] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample), std::bit_cast(slot.whiteNits)}; draw(commands, slot, entry, slot.dumpFramebuffer, extent, parameters); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 45ccc24d26..d3149bd710 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -20,6 +20,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" @@ -2817,13 +2818,14 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } } else { if (display != nullptr) { - state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat)); + const bool pq = DisplayPq(display->pixelFormat); + state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat) && !pq, DisplayHalfFloat(display->pixelFormat), pq ? std::optional(SdrToneMapLuminanceNits) : std::nullopt); } if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } if (drawn) { - state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, residentLayer); + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, SdrToneMapLuminanceNits, residentLayer); state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { state->scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index 95486e0e6c..672a396493 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include namespace AgcDriver::Graphics { @@ -13,7 +14,7 @@ public: GpuColorTransfer(const GpuColorTransfer&) = delete; GpuColorTransfer& operator=(const GpuColorTransfer&) = delete; void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes = 4); - void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false); + void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false, std::uint32_t layer = 0); void Tile(VkCommandBuffer commands); void WriteBack(std::uint64_t address); @@ -22,8 +23,8 @@ public: private: void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes); - void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false, bool halfFloat = false); - void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat); + void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); + void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits); void release() noexcept; Context context; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; @@ -36,6 +37,7 @@ private: std::unique_ptr tiledDevice; std::unique_ptr linear; std::unique_ptr thresholds; + std::unique_ptr pqNits; std::uint32_t width = 0; std::uint32_t height = 0; ColorTileMode mode = ColorTileMode::Linear; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp index c494b7aee9..7e787cb840 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp @@ -1,14 +1,19 @@ #version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" layout(local_size_x = 8, local_size_y = 8) in; layout(set = 0, binding = 0, std430) buffer TiledPixels { uint tiled[]; }; layout(set = 0, binding = 1, std430) buffer LinearPixels { uint linear[]; }; layout(set = 0, binding = 2, std430) readonly buffer ScanoutThresholds { uint thresholds[]; }; +layout(set = 0, binding = 3, std430) readonly buffer PqLuminance { float pqNits[]; }; layout(push_constant) uniform Parameters { uint width; uint height; uint blocksPerRow; uint mode; + float whiteNits; } parameters; uint scanoutCode(uint bits, uint base) { @@ -45,6 +50,13 @@ void main() { } else { pixel = (parameters.mode & 1u) != 0u ? linear[linearIndex] : tiled[tiledIndex]; } + if ((parameters.mode & 32u) != 0u) { + vec3 nits = vec3(pqNits[pixel & 0x3ffu], pqNits[(pixel >> 10u) & 0x3ffu], pqNits[(pixel >> 20u) & 0x3ffu]); + vec3 encoded = srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)); + uvec3 codes = uvec3(floor(encoded * 255.0 + 0.5)); + linear[linearIndex] = codes.b | (codes.g << 8u) | (codes.r << 16u) | 0xff000000u; + return; + } // Mode 8: A2B10G10R10 source (red in the low bits) presented as 8-bit RGBA. if ((parameters.mode & 8u) != 0u) pixel = ((pixel >> 2u) & 0xffu) | (((pixel >> 12u) & 0xffu) << 8u) | (((pixel >> 22u) & 0xffu) << 16u) | 0xff000000u; if ((parameters.mode & 2u) != 0u) pixel = (pixel & 0xff00ff00u) | ((pixel & 0xffu) << 16u) | ((pixel >> 16u) & 0xffu); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl new file mode 100644 index 0000000000..4a35119dea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl @@ -0,0 +1,28 @@ +float pqToneMap(float luminance, float relativeWhite) { + const float knee = 0.75; + if (luminance <= knee) return luminance; + float t = (luminance - knee) / (1.0 - knee); + float w = (relativeWhite - knee) / (1.0 - knee); + return knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); +} + +vec3 pqToSdrLinear(vec3 nits, float whiteNits) { + float referenceWhite = min(203.0, whiteNits); + vec3 bt2020 = nits / referenceWhite; + vec3 rgb = max(vec3( + 1.6604910 * bt2020.r - 0.5876411 * bt2020.g - 0.0728499 * bt2020.b, + -0.1245505 * bt2020.r + 1.1328999 * bt2020.g - 0.0083494 * bt2020.b, + -0.0181508 * bt2020.r - 0.1005789 * bt2020.g + 1.1187297 * bt2020.b), vec3(0.0)); + float luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b; + if (luminance > 0.0) rgb = min(rgb * (pqToneMap(luminance, whiteNits / referenceWhite) / luminance), vec3(1.0)); + return rgb; +} + +float srgbEncode(float linear) { + float v = clamp(linear, 0.0, 1.0); + return v <= 0.0031308 ? 12.92 * v : 1.055 * pow(v, 1.0 / 2.4) - 0.055; +} + +vec3 srgbEncode(vec3 linear) { + return vec3(srgbEncode(linear.r), srgbEncode(linear.g), srgbEncode(linear.b)); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag index 9e60e4e22c..ccd35c25ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag @@ -1,16 +1,25 @@ #version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" layout(set = 0, binding = 0) uniform usampler2D source; +layout(set = 0, binding = 1, std430) readonly buffer PqLuminance { float pqNits[]; }; layout(push_constant) uniform Parameters { ivec2 origin; ivec2 size; ivec2 sourceSize; int stride; uint mode; + float whiteNits; } parameters; layout(location = 0) out vec4 color; vec4 decode(uint word) { + if ((parameters.mode & 16u) != 0u) { + vec3 nits = vec3(pqNits[word & 0x3ffu], pqNits[(word >> 10u) & 0x3ffu], pqNits[(word >> 20u) & 0x3ffu]); + return vec4(srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)), 1.0); + } uvec4 channels; float levels = 255.0; if ((parameters.mode & 1u) != 0u) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 8bce1cc4f6..973aff1e34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -1,8 +1,11 @@ #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/ColorTransfer_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include +#include +#include #include namespace AgcDriver::Graphics { @@ -10,7 +13,7 @@ namespace AgcDriver::Graphics { GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { VkShaderModule module = VK_NULL_HANDLE; try { - std::array bindings{}; + std::array bindings{}; for (std::uint32_t i = 0; i < bindings.size(); ++i) { bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; } @@ -18,7 +21,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { descriptorInfo.bindingCount = static_cast(bindings.size()); descriptorInfo.pBindings = bindings.data(); Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout color transfer"); - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 16}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 20}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; layoutInfo.setLayoutCount = 1; layoutInfo.pSetLayouts = &descriptorLayout; @@ -41,7 +44,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; - const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}; + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4}; VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.maxSets = 1; poolInfo.poolSizeCount = 1; @@ -55,14 +58,20 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { const auto& table = HalfFloatScanoutThresholds(); thresholds = std::make_unique(context, sizeof(table), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memcpy(thresholds->Bytes().data(), table.data(), sizeof(table)); - const VkDescriptorBufferInfo thresholdInfo{thresholds->Handle(), 0, sizeof(table)}; - VkWriteDescriptorSet thresholdWrite{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - thresholdWrite.dstSet = descriptorSet; - thresholdWrite.dstBinding = 2; - thresholdWrite.descriptorCount = 1; - thresholdWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - thresholdWrite.pBufferInfo = &thresholdInfo; - context.Function("vkUpdateDescriptorSets")(context.device, 1, &thresholdWrite, 0, nullptr); + const auto& luminance = AgcDriver::PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const std::array tableInfo{{{thresholds->Handle(), 0, sizeof(table)}, {pqNits->Handle(), 0, sizeof(luminance)}}}; + std::array tableWrites{}; + for (std::uint32_t i = 0; i < tableWrites.size(); ++i) { + tableWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + tableWrites[i].dstSet = descriptorSet; + tableWrites[i].dstBinding = 2 + i; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + tableWrites[i].pBufferInfo = &tableInfo[i]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); } catch (...) { if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); release(); @@ -118,27 +127,28 @@ void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std GuestMemory::Read(address, tiled->Bytes(), layout.Alignment()); } -void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool halfFloat) { +void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { Require(commands != VK_NULL_HANDLE && tiled && tiledDevice && linear, "color transfer is not prepared"); Require(halfFloat == (elementBytes == 8u), "color transfer source does not match the prepared texel size"); Require(!toTiled || !halfFloat, "color transfer cannot write 16-bit float texels back to the guest"); // The guest bytes move to video memory with one DMA copy; the shader's scattered accesses stay local. RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CopyBuffer(context, commands, tiled->Handle(), 0, tiledDevice->Handle(), 0, tiledDevice->Size()); - dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget, halfFloat); + dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget, halfFloat, pqWhiteNits); if (toTiled) { CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); } } -void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat) { +void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits) { + Require(!pqWhiteNits || (!toTiled && !tenBit && !halfFloat && elementBytes == 4u && std::isfinite(*pqWhiteNits) && *pqWhiteNits >= 1.0f), "color transfer converts PQ texels of 4 bytes to an 8-bit linear output with a white level of at least 1 nit"); VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; before.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; before.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; const auto barrier = context.Function("vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); - const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u) | (halfFloat ? 16u : 0u)}; + const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u) | (halfFloat ? 16u : 0u) | (pqWhiteNits ? 32u : 0u), std::bit_cast(pqWhiteNits.value_or(0.0f))}; context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); @@ -158,11 +168,11 @@ void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkIm copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; copy.imageExtent = {width, height, 1}; context.Function("vkCmdCopyImageToBuffer")(commands, image, layout, tiledDevice->Handle(), 1, ©); - dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat); + dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat, std::nullopt); } -void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit, bool halfFloat) { - convert(commands, false, swapRedBlue, tenBit, halfFloat); +void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { + convert(commands, false, swapRedBlue, tenBit, halfFloat, pqWhiteNits); } void GpuColorTransfer::Tile(VkCommandBuffer commands) { diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 4df2315361..736f3207fd 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -6,7 +6,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" COMMAND "$" -V --target-env vulkan1.1 -o "${agcColorTransferSpv}" "${agcColorTransferSource}" COMMAND ${CMAKE_COMMAND} -DINPUT=${agcColorTransferSpv} -DOUTPUT=${agcColorTransferHeader} -DSYMBOL=COLOR_TRANSFER_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" - DEPENDS "${agcColorTransferSource}" glslang-standalone + DEPENDS "${agcColorTransferSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone VERBATIM ) diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp index f5f1f159ff..a257da198b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -264,6 +264,14 @@ int main() { ExpectPixels(buffer, DccKeys::ClearRegister, {0, 0, 255, 255}, "A2B10G10R10 register clear"); buffer.pixelFormat = bgra | tenBit; ExpectPixels(buffer, DccKeys::ClearRegister, {255, 0, 0, 255}, "A2R10G10B10 register clear"); + buffer.pixelFormat = 0x8100070422000000ull; + ExpectPixels(buffer, DccKeys::Clear0000, {0, 0, 0, 255}, "R10G10B10A2 PQ 0000"); + ExpectPixels(buffer, DccKeys::Clear1111, {255, 255, 255, 255}, "R10G10B10A2 PQ 1111"); + buffer.dccClearColor = 600u | (500u << 10u) | (300u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "R10G10B10A2 PQ register clear"); + buffer.pixelFormat = 0x8100070400000000ull; + buffer.dccClearColor = 300u | (500u << 10u) | (600u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "B10G10R10A2 PQ register clear"); for (const auto keys : {DccKeys::Uncompressed, DccKeys::Mixed, DccKeys::Unreadable}) { const auto message = Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, keys); }); Expect(message.find(AgcDriver::Graphics::DccKeysName(keys)) != std::string::npos && message.find("0x7f0000") != std::string::npos, std::string("keys without a clear value were presented as one: ") + AgcDriver::Graphics::DccKeysName(keys)); diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp index 94194bd4b2..da2323b3de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -29,6 +30,8 @@ constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; constexpr std::uint64_t TenBit = 0x0100000000000000ull; constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; class Device { public: @@ -272,11 +275,11 @@ std::vector convertPathPresent(const Context& context, const Source& return bytesOf(readback, bytes); } -std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0) { +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); const auto bytes = static_cast(extent.width) * extent.height * 4; Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat, layer); + pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); { CommandBatch batch(context); const auto commands = batch.Handle(); @@ -288,11 +291,11 @@ std::vector passPresent(const Context& context, PresentationPass& pas return bytesOf(readback, bytes); } -std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride, std::uint32_t layer = 0) { +std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); const auto bytes = static_cast(extent.width) * extent.height * 4; Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat, layer); + pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); { CommandBatch batch(context); pass.RecordDump(batch.Handle(), 1, stride, readback.Handle()); @@ -303,7 +306,7 @@ std::vector passDump(const Context& context, PresentationPass& pass, } std::string describe(const Source& source, VkExtent2D extent) { - return std::string(AgcDriver::DisplayTenBit(source.pixelFormat) ? "10-bit " : "8-bit ") + (AgcDriver::DisplayRedLow(source.pixelFormat) ? "red-low" : "blue-low") + " display in VkFormat " + std::to_string(static_cast(source.storage)) + " at " + std::to_string(extent.width) + "x" + std::to_string(extent.height); + return std::string(AgcDriver::DisplayPq(source.pixelFormat) ? "PQ " : AgcDriver::DisplayTenBit(source.pixelFormat) ? "10-bit " : "8-bit ") + (AgcDriver::DisplayRedLow(source.pixelFormat) ? "red-low" : "blue-low") + " display in VkFormat " + std::to_string(static_cast(source.storage)) + " at " + std::to_string(extent.width) + "x" + std::to_string(extent.height); } void passMatchesConvertPath(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { @@ -433,11 +436,56 @@ void passPresentsSlice(const Context& context, PresentationPass& pass) { void passRefusesHalfFloat(PresentationPass& pass, const Image& resident) { bool refused = false; try { - pass.Bind(1, resident.Handle(), 200, 130, Rgba16Float); + pass.Bind(1, resident.Handle(), 200, 130, Rgba16Float, AgcDriver::SdrToneMapLuminanceNits); } catch (const std::exception&) { refused = true; } Require(refused, "the presentation pass took a 16-bit float display buffer"); + refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgb10Pq, 0.0f); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a white level below 1 nit"); +} + +int largestDifference(const std::vector& expected, const std::vector& actual) { + Require(expected.size() == actual.size(), "compared frames differ in size"); + int worst = 0; + for (std::size_t i = 0; i < expected.size(); ++i) worst = std::max(worst, std::abs(std::to_integer(expected[i]) - std::to_integer(actual[i]))); + return worst; +} + +Source convertedAt(const Source& source, float whiteNits) { + auto converted = source; + for (std::size_t i = 0; i < converted.decoded.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, source.words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(source.pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) converted.decoded[i + channel] = static_cast(pixel[channel]); + } + return converted; +} + +void passConvertsPq(const Context& context, PresentationPass& pass, const Source& guest, const Image& resident, float whiteNits) { + const auto source = convertedAt(guest, whiteNits); + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}, VkExtent2D{source.width, source.height + 51}}) { + Require(largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)) <= 1, "the PQ presentation pass differs from the CPU conversion by more than one level: " + describe(source, extent)); + } + for (const auto extent : {VkExtent2D{117, 76}, VkExtent2D{331, 215}}) { + const int worst = largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)); + std::printf(" %s, white %.0f nits: scaled, by at most %d\n", describe(source, extent).c_str(), whiteNits, worst); + Require(worst <= 2, "the scaled PQ presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } + for (const std::uint32_t stride : {1u, 3u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(largestDifference(expected, passDump(context, pass, source, resident, stride, 0, whiteNits)) <= 1, "the PQ presentation dump differs from the subsampled CPU conversion: " + describe(source, extent) + " stride " + std::to_string(stride)); + } } void residentPresentTests(const Context& context) { @@ -457,6 +505,12 @@ void residentPresentTests(const Context& context) { if (AgcDriver::DisplayTenBit(pixelFormat)) passKeepsTenBits(context, pass, source, *resident); if (pixelFormat == Bgra8) passRefusesHalfFloat(pass, *resident); } + for (const auto& [pixelFormat, storage] : {std::pair{Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}}) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passConvertsPq(context, pass, source, *resident, AgcDriver::SdrToneMapLuminanceNits); + passConvertsPq(context, pass, source, *resident, 1000.0f); + } passPresentsSlice(context, pass); blitLetterboxMatchesClear(context); } diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp index 718a636ed6..e52bb5b232 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -1,6 +1,7 @@ #include "ResidentPresent.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +28,8 @@ constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; constexpr std::uint64_t TenBit = 0x0100000000000000ull; constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; void decisionTests() { using AgcDriver::DisplayTexelFormat; @@ -42,6 +46,13 @@ void decisionTests() { } Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a 10-bit display"); } + Require(DisplayTexelFormat(Bgr10Pq) == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && DisplayTexelFormat(Rgb10Pq) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "PQ display texel formats are wrong"); + for (const auto display : {Bgr10Pq, Rgb10Pq}) { + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + Require(ResidentPresentPath(storage, display, true) == ResidentPresent::Convert, "a PQ image is blitted (it must be converted to SDR)"); + } + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a PQ display"); + } Require(ResidentPresentPath(VK_FORMAT_A2B10G10R10_UNORM_PACK32, Bgra8, true) == ResidentPresent::None && ResidentPresentPath(VK_FORMAT_R16G16_SFLOAT, Bgra8, true) == ResidentPresent::None, "an image of another type presents an 8-bit display"); const auto guest = FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4); Require(guest.has_value() && guest == FindGuestTextureFormat(VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4), "the blue-low 10-bit color target has no storage format"); @@ -70,7 +81,7 @@ void sliceTests() { Require(!ResidentPresentSlice(image, 0, 0, image, slice) && !ResidentPresentSlice(image, slice, 1, image, 0), "an empty image or display buffer has a slice"); } -void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format, std::uint32_t layer = 0) { +void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { constexpr std::uint32_t width = 200; constexpr std::uint32_t height = 130; const auto texelBytes = AgcDriver::DisplayTexelBytes(pixelFormat); @@ -93,7 +104,15 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat for (std::uint32_t x = 0; x < width; ++x) std::memcpy(guest.data() + AgcDriver::DisplayBufferOffset(display, x, y), words.data() + (static_cast(y) * width + x) * texelBytes, texelBytes); } } - const auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + if (AgcDriver::DisplayPq(pixelFormat) && whiteNits != AgcDriver::SdrToneMapLuminanceNits) { + for (std::size_t i = 0; i < expected.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) expected[i + channel] = static_cast(pixel[channel]); + } + } VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; @@ -131,8 +150,10 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat Buffer fromGuest(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); GpuColorTransfer transfer(context); const bool redLow = AgcDriver::DisplayRedLow(pixelFormat); - const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat); + const bool pq = AgcDriver::DisplayPq(pixelFormat); + const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat) && !pq; const bool halfFloat = AgcDriver::DisplayHalfFloat(pixelFormat); + const auto pqWhiteNits = pq ? std::optional(whiteNits) : std::nullopt; const auto barrier = context.Function("vkCmdPipelineBarrier"); const auto readback = [&](VkCommandBuffer commands, Buffer& destination) { const VkBufferCopy copy{0, 0, expected.size()}; @@ -166,15 +187,20 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); - transfer.Detile(commands, redLow, tenBit, halfFloat); + transfer.Detile(commands, redLow, tenBit, halfFloat, pqWhiteNits); readback(commands, fromGuest); - transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat, layer); - readback(commands, fromImage); + if (!pq) { + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat, layer); + readback(commands, fromImage); + } batch.SubmitAndWait(); } - const auto name = std::string(halfFloat ? "16-bit float " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from layer " + std::to_string(layer) + " of VkFormat " + std::to_string(static_cast(format)); - Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "the GPU detile differs from the CPU decode: " + name); - Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "the resident image presents other pixels than guest memory: " + name); + const auto name = std::string(halfFloat ? "16-bit float " : pq ? "PQ " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from layer " + std::to_string(layer) + " of VkFormat " + std::to_string(static_cast(format)); + const auto matches = [&](std::span actual) { + return std::equal(expected.begin(), expected.end(), actual.begin(), [&](std::byte want, std::byte got) { return std::abs(std::to_integer(want) - std::to_integer(got)) <= (pq ? 1 : 0); }); + }; + Require(matches(fromGuest.Bytes()), "the GPU detile differs from the CPU decode: " + name); + Require(pq || matches(fromImage.Bytes()), "the resident image presents other pixels than guest memory: " + name); } } @@ -190,5 +216,9 @@ void RunResidentPresentTests(const Context& context) { conversionTest(context, Rgba16Float, VK_FORMAT_R16G16B16A16_SFLOAT); conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM, 1); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 2); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 0, 1000.0f); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32, 0, 150.0f); std::cout << "resident present format and conversion tests passed\n"; } diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9fed682ae0..0db2c17be3 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -21,6 +21,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" namespace { @@ -225,9 +226,8 @@ int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLumi if (luminance == nullptr) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - constexpr float SdrReferenceWhiteNits = 100.0f; - luminance->max_full_frame_tone_map_luminance = SdrReferenceWhiteNits; - luminance->max_tone_map_luminance = SdrReferenceWhiteNits; + luminance->max_full_frame_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; + luminance->max_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; luminance->min_tone_map_luminance = 0.0f; return SYSTEM_SERVICE_OK; } diff --git a/core/libs/tests/LinearDisplayBuffer.cpp b/core/libs/tests/LinearDisplayBuffer.cpp index d34623bd0e..ff5e5da9b8 100644 --- a/core/libs/tests/LinearDisplayBuffer.cpp +++ b/core/libs/tests/LinearDisplayBuffer.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include @@ -86,6 +87,89 @@ static void HalfFloatDisplay() { } } +static double ReferencePqNits(std::uint32_t code) { + const double m1 = 2610.0 / 16384.0, m2 = 2523.0 / 4096.0 * 128.0, c1 = 3424.0 / 4096.0, c2 = 2413.0 / 4096.0 * 32.0, c3 = 2392.0 / 4096.0 * 32.0; + const double e = std::pow(code / 1023.0, 1.0 / m2); + return 10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1); +} + +static std::array ReferenceSdr(std::uint32_t red, std::uint32_t green, std::uint32_t blue, double whiteNits) { + const double referenceWhite = std::min(203.0, whiteNits); + const double r = ReferencePqNits(red) / referenceWhite, g = ReferencePqNits(green) / referenceWhite, b = ReferencePqNits(blue) / referenceWhite; + std::array rgb{std::max(1.6604910 * r - 0.5876411 * g - 0.0728499 * b, 0.0), std::max(-0.1245505 * r + 1.1328999 * g - 0.0083494 * b, 0.0), std::max(-0.0181508 * r - 0.1005789 * g + 1.1187297 * b, 0.0)}; + const double luminance = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; + const double knee = 0.75; + if (luminance > knee) { + const double t = (luminance - knee) / (1.0 - knee), w = (whiteNits / referenceWhite - knee) / (1.0 - knee); + const double mapped = knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); + for (auto& channel : rgb) channel *= mapped / luminance; + } + for (auto& channel : rgb) { + const double v = std::clamp(channel, 0.0, 1.0); + channel = 255.0 * (v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1.0 / 2.4) - 0.055); + } + return rgb; +} + +static std::uint32_t PqWord(std::uint32_t red, std::uint32_t green, std::uint32_t blue, bool redLow) { + return (redLow ? red : blue) | (green << 10u) | ((redLow ? blue : red) << 20u) | (3u << 30u); +} + +static void PqDisplay() { + constexpr std::uint64_t rgbPq = 0x8100070422000000ull; + constexpr std::uint64_t bgrPq = 0x8100070400000000ull; + for (const auto pixelFormat : {rgbPq, bgrPq}) { + const bool redLow = pixelFormat == rgbPq; + Require(AgcDriver::DisplayPq(pixelFormat) && AgcDriver::DisplayTenBit(pixelFormat) && !AgcDriver::DisplayHalfFloat(pixelFormat) && AgcDriver::DisplayRedLow(pixelFormat) == redLow); + Require(AgcDriver::DisplayTexelBytes(pixelFormat) == 4 && AgcDriver::DisplayTexelFormat(pixelFormat) == (redLow ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32)); + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) Require(AgcDriver::ResidentPresentPath(storage, pixelFormat, true) == AgcDriver::ResidentPresent::Convert); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, pixelFormat, true) == AgcDriver::ResidentPresent::None && AgcDriver::ResidentPresentPath(VK_FORMAT_R16G16B16A16_SFLOAT, pixelFormat, true) == AgcDriver::ResidentPresent::None); + Require(AgcDriver::DisplayBufferSize({65536, pixelFormat, 3840, 2160}) == 30u * 17u * 65536u); + struct Anchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array anchors{{{0, 0, 0, 0, 0, 0}, {1023, 1023, 1023, 255, 255, 255}, {520, 520, 520, 255, 255, 255}, {450, 450, 450, 187, 187, 187}, + {600, 500, 300, 255, 213, 0}, {480, 470, 350, 225, 204, 95}, {350, 420, 500, 16, 167, 241}, {1023, 0, 0, 255, 0, 0}}}; + std::array input{}; + for (std::size_t i = 0; i < anchors.size(); ++i) { + const auto word = PqWord(anchors[i].red, anchors[i].green, anchors[i].blue, redLow); + std::memcpy(input.data() + i * 4, &word, 4); + } + const AgcDriver::DisplayBuffer buffer{65536, pixelFormat, 4, 2, 1, 0}; + Require(AgcDriver::DisplayBufferSize(buffer) == input.size()); + const auto pixels = AgcDriver::DecodeDisplayBuffer(buffer, input); + for (std::size_t i = 0; i < anchors.size(); ++i) { + Require(std::to_integer(pixels[i * 4]) == anchors[i].b && std::to_integer(pixels[i * 4 + 1]) == anchors[i].g && std::to_integer(pixels[i * 4 + 2]) == anchors[i].r && std::to_integer(pixels[i * 4 + 3]) == 255); + } + } + struct WhiteAnchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array shoulder{{{770, 770, 770, 255, 255, 255}, {700, 700, 700, 252, 252, 252}, {650, 650, 650, 249, 249, 249}, {560, 560, 560, 221, 221, 221}, {450, 450, 450, 136, 136, 136}, {600, 500, 300, 255, 155, 0}}}; + for (const auto& anchor : shoulder) { + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(anchor.red, anchor.green, anchor.blue, true), true, 1000.0f); + Require(pixel[2] == anchor.r && pixel[1] == anchor.g && pixel[0] == anchor.b && pixel[3] == 255); + } + for (const auto pixelFormat : {0x8100070000000000ull, 0x8100000400000000ull, 0x8000070400000000ull, 0x8100070400000001ull, 0x8100070600000000ull, 0xc001070700000000ull}) { + Require(!AgcDriver::DisplayPq(pixelFormat)); + Reject([&] { AgcDriver::DisplayBufferSize({65536, pixelFormat, 64, 64}); }); + } + for (const float white : {100.0f, 150.0f, 1000.0f}) { + for (std::uint32_t red = 0; red < 1024; red += 31) { + for (std::uint32_t green = 0; green < 1024; green += 31) { + for (std::uint32_t blue = 0; blue < 1024; blue += 31) { + const auto expected = ReferenceSdr(red, green, blue, white); + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(red, green, blue, true), true, white); + Require(std::fabs(pixel[2] - expected[0]) <= 1.0 && std::fabs(pixel[1] - expected[1]) <= 1.0 && std::fabs(pixel[0] - expected[2]) <= 1.0 && pixel[3] == 255); + Require(AgcDriver::PqToSdrPixel(PqWord(red, green, blue, false), false, white) == pixel); + } + } + } + } +} + int main() { AgcDriver::DisplayBuffer buffer{65536, 0x8000000000000000ull, 2, 2, 1, 3}; const std::array input{1,2,3,4, 5,6,7,8, 99,99,99,99, @@ -110,4 +194,5 @@ int main() { Require(AgcDriver::DisplayBufferSize(buffer) == 65536); HalfFloatCodes(); HalfFloatDisplay(); + PqDisplay(); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 821e37ade6..e151eaebf0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -91,6 +91,7 @@ - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. +- [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. The output is SDR, so they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the tone-map luminance sceSystemServiceGetHdrToneMapLuminance reports. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware From 5dc71119241d4db7a326fed1bbcb797566496003 Mon Sep 17 00:00:00 2001 From: hossiendehghan989 <1.86807511e+08+hossiendehghan989@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:01:05 +0000 Subject: [PATCH 317/353] fix(libSceVideoOut): export unknown Onimusha import NID --- core/libs/prx/libSceVideoOut/Unimplemented.cpp | 8 ++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..24bf6dbc9d 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,6 +7,14 @@ extern "C" { +APS5_EXPORT("YNw9Xk5iNWo", sceVideoOutUnknown_YNw9Xk5iNWo); +int APS5_VABI sceVideoOutUnknown_YNw9Xk5iNWo(int handle, void* status) { + (void)handle; + (void)status; + NotImplemented_nid_no_patch("YNw9Xk5iNWo"); + return 0; +} + int APS5_VABI sceVideoOutSetFlipMaster() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ce195aa306..ac9a3fb89a 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -8,6 +8,7 @@ ### Unknown function info +- [sceVideoOutUnknown_YNw9Xk5iNWo](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `YNw9Xk5iNWo`) - imported by Onimusha Way of the Sword (PPSA27836) on a newer SDK. The observed call shape is `(handle, status)` based on the import site, but the function's semantic name, output layout, and console behaviour are unknown; it is exposed only to let module loading reach the next unresolved dependency and reports an explicit unimplemented call when invoked - [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 - [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 From f288daf6fe1b43b6b3959f99e6b73f54879f747b Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 09:11:27 +0100 Subject: [PATCH 318/353] feat(libkernel): select on pipes on Windows select now checks pipe descriptors on Windows with the readiness test poll already uses, so a pipe next to a libSceNet socket no longer throws. RetroArch (PPSA99169) selects on libmicrohttpd's wake-up pipe beside its listening socket. guest_select now runs on Windows too. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 16 +++++---- .../Socket/include/SocketRuntime.hpp | 3 ++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 33 ++++++++++++++----- core/libs/prx/libkernel/Socket/src/Udp.cpp | 6 ++++ docs/dev/debt/libkernel.md | 4 +-- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2c31e15178..9d8f71003a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1066,13 +1066,15 @@ if(BUILD_TESTING) set_tests_properties(guest_select_wait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) - target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_select_tests PRIVATE libkernel libc) - configure_windows_unwind(guest_select_tests) - add_test(NAME guest_select COMMAND guest_select_tests) - set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) + target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_tests) + add_test(NAME guest_select COMMAND guest_select_tests) + set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_tcp_tests ${LIBS_INCLUDE_DIR}/tests/GuestTcp.cpp) target_include_directories(guest_tcp_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 61ee521433..a39295ecd6 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -21,6 +21,9 @@ std::int64_t Write(int descriptor, const void* buffer, std::size_t length); int Stat(int descriptor, FileStat* status); bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof); int Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds); +#ifdef _WIN32 +bool PipeReady(int descriptor, short events, short& revents); +#endif struct Interest { int descriptor; bool write; diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 9f4b719e5b..e01a8b7f34 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" @@ -21,6 +22,7 @@ namespace { +constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; constexpr int SelectDescriptorLimit = 1024; constexpr int SelectPollSliceMilliseconds = 10; @@ -38,8 +40,8 @@ bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } -void UnknownDescriptor(int descriptor, bool polled) { - const auto what = "select on descriptor " + std::to_string(descriptor) + (polled ? " (not a libSceNet socket)" : " (libSceNet is not loaded)"); +void UnknownDescriptor(int descriptor) { + const auto what = "select on descriptor " + std::to_string(descriptor) + " (not a libSceNet socket)"; NotImplemented_nid_no_patch(what.c_str()); } @@ -91,6 +93,25 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise return result; } +#else + +int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMilliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMilliseconds); + for (;;) { + int ready = 0; + for (int index = 0; index < count; ++index) { + auto& entry = descriptors[index]; + if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) return -GuestEbadf; + if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) { + NotImplemented_nid_no_patch(("select on descriptor " + std::to_string(entry.descriptor) + " (neither a socket nor a pipe)").c_str()); + } + if (entry.revents != 0) ++ready; + } + if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + #endif } @@ -165,10 +186,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* std::vector groups; if (!kernel.entries.empty()) groups.push_back(std::move(kernel)); if (!net.entries.empty()) { - PollGroup hosts{nullptr, {}, {}}; -#ifndef _WIN32 - hosts.poll = &HostPoll; -#endif + PollGroup hosts{&HostPoll, {}, {}}; net.poll = g_socketPoller.load(); if (net.poll != nullptr) { for (auto& socket : net.entries) socket.revents = 0; @@ -184,7 +202,6 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* continue; } const int descriptor = net.entries[index].descriptor; - if (hosts.poll == nullptr) UnknownDescriptor(descriptor, net.poll != nullptr); hosts.entries.push_back({descriptor, net.entries[index].events, 0}); hosts.slots.push_back(net.slots[index]); net.entries.erase(net.entries.begin() + static_cast(index)); @@ -226,7 +243,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* } int ready = 0; for (const auto& entry : requested) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor, true); + if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor); const bool readable = (entry.events & KernelSocketPoll::Readable) != 0 && (entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) != 0; const bool writable = (entry.events & KernelSocketPoll::Writable) != 0 && (entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) != 0; const bool urgent = (entry.events & KernelSocketPoll::Urgent) != 0 && (entry.revents & KernelSocketPoll::Urgent) != 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7537e617df..e33cc71239 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -474,6 +474,12 @@ bool IsPipe(int descriptor, short events) { #endif } +#ifdef _WIN32 +bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { + return PipeReadiness(descriptor, events, revents); +} +#endif + int GuestSockets::Duplicate(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 709e78c6d4..92acf2e599 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,8 +59,8 @@ - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors throw on Windows and are polled through the host on Linux. Dungeons (PPSA16064) selects on descriptor 0 -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing, except on Windows where it still throws; an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified From 3eb19d9f2375f26948f63417eb195e7cf0454a40 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 20:27:02 +1300 Subject: [PATCH 319/353] fix(agc): keep the saved guest bytes of units a fill or image copy does not cover A fill turned into an image clear or an image copy invalidated the saved guest bytes of every unit of each other image over its range. The flush before it stores only the units it overlaps, so results still pending in the other units lost their saved bytes, and storing them over memory outside the write watch threw ("storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"). Only the units over the filled or copied range lose their saved bytes now; the others are compared with them at their store as before. --- .../Graphics/include/Texture.hpp | 3 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 13 +++- .../tests/execution/StoreOverUntracked.cpp | 65 ++++++++++++++++--- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 93f60adcdf..1d901533e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -369,6 +369,9 @@ private: void saveBeforeUnwatch(); void forgetKeptAtUnwatch(std::uint32_t unit); bool keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const; + // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill + // or copy supersedes them there); the other units' bytes stay valid. + void forgetSavedOver(std::uint64_t address, std::size_t bytes); bool guestBytesSettled() const; // Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB // aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d650643804..b64c0e51ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3222,6 +3222,15 @@ bool StorageTexture::keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) return found != keptAtUnwatch.end() && layerOriginal[unit] && found->second == layerGeneration[unit]; } +void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { + // Only these units' bytes changed under the saved copy. Units outside keep it: results still + // pending there (the flush before the fill or copy stored only the units it overlaps) are + // compared with it at their store when the memory is outside the write watch. + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) layerOriginal[unit] = false; + } +} + void StorageTexture::saveBeforeUnwatch() { std::vector units(trackedLayers, false); for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit] && layerGeneration[unit] != 0; @@ -3894,7 +3903,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue; - texture->layerOriginal.assign(texture->trackedLayers, false); + texture->forgetSavedOver(begin, bytes); if (texture->descriptor.dccAddress != 0) aliasedKeys = true; } } @@ -4028,7 +4037,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { auto& live = Live(); std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { - if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false); + if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->forgetSavedOver(descriptor.baseAddress, static_cast(guestBytes)); } } GuestMemory::MarkWritten(descriptor.baseAddress, static_cast(guestBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index f7f8b8105c..99e31a350a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -57,12 +57,12 @@ std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } -std::array TextureDescriptor(const std::uint32_t* texels) { +std::array TextureDescriptor(const std::uint32_t* texels, std::uint32_t side = Side) { 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), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -91,9 +91,9 @@ void Dispatch(std::span code, std::uint32_t threads, std::u AgcDriver::Submit(&packet, 0); } -void Store(const std::uint32_t* texels) { +void Store(const std::uint32_t* texels, std::uint32_t side = Side) { std::array userData{}; - const auto texture = TextureDescriptor(texels); + const auto texture = TextureDescriptor(texels, side); std::copy(texture.begin(), texture.end(), userData.begin() + 8); Dispatch(StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); } @@ -205,7 +205,11 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch }; +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied }; + +// One 64 KiB block of the surface: the image of its last block, and the source copied into it. +constexpr std::uint32_t BlockSide = 128; +constexpr std::size_t LastBlock = SurfaceBytes - Block; struct Case { const char* name; @@ -217,7 +221,7 @@ enum class Outcome { Kept, Lost, Missed }; Outcome Run(const Case& test) { std::optional source; - if (test.kind == Kind::Copied) source.emplace(); + if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch; Surface surface(leaves); auto* bytes = surface.Bytes(); @@ -250,6 +254,50 @@ Outcome Run(const Case& test) { std::fprintf(stderr, "[store over untracked] %s: not run, the surface has no host import\n", test.name); return Outcome::Kept; } + } else if (test.kind == Kind::NeighbourCopied) { + // The surface's image has results pending in its first block when a copy replaces the + // image of its last block: the copy supersedes the saved bytes there only. + const auto settledLive = [&](std::uint64_t at, std::size_t size) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + StorageTexture::FlushAllPending("store over untracked fixture"); + const bool live = StorageTexture::FindLive(at, size) != nullptr; + hold.unlock(); + if (live) return; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the block's storage image was never created"); + std::this_thread::yield(); + } + }; + Store(surface.Texels() + LastBlock / 4u, BlockSide); + settledLive(address + LastBlock, Block); + Store(source->Texels(), BlockSide); + settledLive(source->Address(), Block); + std::fill(surface.Texels(), surface.Texels() + SurfaceBytes / 4u, Untouched); + Store(surface.Texels()); + // Pending results are the surface image's alone once it exists. + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr) { + if (StorageTexture::AnyPending()) break; + hold.unlock(); + return Outcome::Missed; + } + hold.unlock(); + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's storage image was never created"); + std::this_thread::yield(); + } + const auto from = StorageTexture::FindLive(source->Address(), Block); + const auto to = StorageTexture::FindLive(address + LastBlock, Block); + Require(from != nullptr && to != nullptr, "store over untracked: the blocks have no storage images to copy between"); + const char* refusal = nullptr; + if (!to->CopyFrom(*from, refusal)) { + Require(refusal != nullptr && std::string_view(refusal) == "not imported", "store over untracked: the image copy was refused"); + StorageTexture::FlushAllPending("store over untracked fixture"); + std::fprintf(stderr, "[store over untracked] %s: not run, the block has no host import\n", test.name); + return Outcome::Kept; + } } else if (!HoldPending(hold, surface, Stored)) { return Outcome::Missed; } @@ -300,7 +348,7 @@ int main() { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, @@ -308,6 +356,7 @@ int main() { {"a surface that left the write watch", Kind::LeftWatch, 0xe000}, {"a surface written before it left the write watch", Kind::WrittenThenLeftWatch, 0xe000}, {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, + {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { From 83a268a8678b364b3edef6451906afba1a7fd1ce Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 20:29:57 +1300 Subject: [PATCH 320/353] feat(libkernel): stop at start-up on a host CPU without the PS5's extensions Titles run natively, so a host CPU without an extension that compiled Zen 2 code uses faults with an illegal instruction deep inside a title (#3719: SHLX, BMI2). libkernel now reads CPUID and XCR0 when it loads on Linux and Windows, logs the host CPU's extensions and throws, naming the CPU and the missing extensions, before any title code runs. --- core/libs/prx/libkernel/CMakeLists.txt | 10 + .../prx/libkernel/System/include/HostCpu.hpp | 31 +++ .../libs/prx/libkernel/System/src/HostCpu.cpp | 199 ++++++++++++++++++ .../System/src/SystemConfiguration.cpp | 18 +- core/libs/tests/HostCpu.cpp | 139 ++++++++++++ docs/dev/debt/libkernel.md | 3 +- docs/user/USAGE.md | 2 + 7 files changed, 384 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libkernel/System/include/HostCpu.hpp create mode 100644 core/libs/prx/libkernel/System/src/HostCpu.cpp create mode 100644 core/libs/tests/HostCpu.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2c31e15178..4262c54d72 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/HostCpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp @@ -975,6 +976,15 @@ if(BUILD_TESTING) set_tests_properties(guest_jit_shared_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(host_cpu_tests ${LIBS_INCLUDE_DIR}/tests/HostCpu.cpp) + target_include_directories(host_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(host_cpu_tests PRIVATE libkernel libc) + configure_windows_unwind(host_cpu_tests) + add_test(NAME host_cpu COMMAND host_cpu_tests) + if(WIN32) + set_tests_properties(host_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_configuration_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemConfiguration.cpp) target_include_directories(guest_system_configuration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/include/HostCpu.hpp b/core/libs/prx/libkernel/System/include/HostCpu.hpp new file mode 100644 index 0000000000..11b3368dbf --- /dev/null +++ b/core/libs/prx/libkernel/System/include/HostCpu.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP + +#include +#include +#include + +struct HostCpuRegisters { + std::uint32_t Leaf1Ecx = 0; + std::uint32_t Leaf1Edx = 0; + std::uint32_t Leaf7Ebx = 0; + std::uint32_t Leaf7Ecx = 0; + std::uint32_t LeafDSub1Eax = 0; + std::uint32_t Leaf80000001Ecx = 0; + std::uint32_t Leaf80000008Ebx = 0; + std::uint64_t Xcr0 = 0; +}; + +struct HostCpuFeature { + const char* Name; + bool Required; + bool Present; +}; + +HostCpuRegisters ReadHostCpuRegisters(); +std::string HostCpuModel(); +std::vector HostCpuFeatures(const HostCpuRegisters& registers); +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers); +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model); + +#endif diff --git a/core/libs/prx/libkernel/System/src/HostCpu.cpp b/core/libs/prx/libkernel/System/src/HostCpu.cpp new file mode 100644 index 0000000000..a27d29b6dc --- /dev/null +++ b/core/libs/prx/libkernel/System/src/HostCpu.cpp @@ -0,0 +1,199 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include "prx/libc/include/general/LogMacros.hpp" +#if defined(__x86_64__) || defined(__i386__) +#include +#endif +#include +#include +#include +#include +#include +#include + +namespace { + +enum class Register : std::uint8_t { + Leaf1Ecx, + Leaf1Edx, + Leaf7Ebx, + Leaf7Ecx, + LeafDSub1Eax, + Leaf80000001Ecx, + Leaf80000008Ebx +}; + +struct FeatureBit { + const char* Name; + Register Source; + unsigned Bit; + bool Required; +}; + +constexpr FeatureBit kFeatures[] = { + {"SSE", Register::Leaf1Edx, 25, true}, + {"SSE2", Register::Leaf1Edx, 26, true}, + {"SSE3", Register::Leaf1Ecx, 0, true}, + {"SSSE3", Register::Leaf1Ecx, 9, true}, + {"SSE4.1", Register::Leaf1Ecx, 19, true}, + {"SSE4.2", Register::Leaf1Ecx, 20, true}, + {"POPCNT", Register::Leaf1Ecx, 23, true}, + {"LZCNT", Register::Leaf80000001Ecx, 5, true}, + {"BMI1", Register::Leaf7Ebx, 3, true}, + {"BMI2", Register::Leaf7Ebx, 8, true}, + {"MOVBE", Register::Leaf1Ecx, 22, true}, + {"CMPXCHG16B", Register::Leaf1Ecx, 13, true}, + {"LAHF/SAHF", Register::Leaf80000001Ecx, 0, true}, + {"AVX", Register::Leaf1Ecx, 28, true}, + {"AVX2", Register::Leaf7Ebx, 5, true}, + {"FMA", Register::Leaf1Ecx, 12, true}, + {"F16C", Register::Leaf1Ecx, 29, true}, + {"AES", Register::Leaf1Ecx, 25, false}, + {"PCLMULQDQ", Register::Leaf1Ecx, 1, false}, + {"RDRAND", Register::Leaf1Ecx, 30, false}, + {"RDSEED", Register::Leaf7Ebx, 18, false}, + {"ADX", Register::Leaf7Ebx, 19, false}, + {"SHA", Register::Leaf7Ebx, 29, false}, + {"CLFLUSHOPT", Register::Leaf7Ebx, 23, false}, + {"CLWB", Register::Leaf7Ebx, 24, false}, + {"RDPID", Register::Leaf7Ecx, 22, false}, + {"XSAVEOPT", Register::LeafDSub1Eax, 0, false}, + {"XSAVEC", Register::LeafDSub1Eax, 1, false}, + {"SSE4A", Register::Leaf80000001Ecx, 6, false}, + {"PREFETCHW", Register::Leaf80000001Ecx, 8, false}, + {"CLZERO", Register::Leaf80000008Ebx, 0, false}, +}; + +constexpr unsigned kOsxsaveBit = 27; +constexpr std::uint64_t kXcr0SseAvxState = 0x6; + +std::uint32_t Value(const HostCpuRegisters& registers, const Register source) { + switch (source) { + case Register::Leaf1Ecx: + return registers.Leaf1Ecx; + case Register::Leaf1Edx: + return registers.Leaf1Edx; + case Register::Leaf7Ebx: + return registers.Leaf7Ebx; + case Register::Leaf7Ecx: + return registers.Leaf7Ecx; + case Register::LeafDSub1Eax: + return registers.LeafDSub1Eax; + case Register::Leaf80000001Ecx: + return registers.Leaf80000001Ecx; + case Register::Leaf80000008Ebx: + return registers.Leaf80000008Ebx; + } + throw std::logic_error("unknown CPUID register"); +} + +bool AvxStateEnabled(const HostCpuRegisters& registers) { + return (registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0 && (registers.Xcr0 & kXcr0SseAvxState) == kXcr0SseAvxState; +} + +std::string DisplayModel(const std::string& model) { + const auto end = model.find_last_not_of(' '); + return end == std::string::npos ? std::string("unknown model") : model.substr(0, end + 1); +} + +std::string Join(const std::vector& names) { + std::string text; + for (const auto& name : names) { + if (!text.empty()) text += ", "; + text += name; + } + return text; +} + +} + +HostCpuRegisters ReadHostCpuRegisters() { + HostCpuRegisters registers; +#if defined(__x86_64__) || defined(__i386__) + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + const unsigned int basic = __get_cpuid_max(0, nullptr); + if (basic >= 1) { + __cpuid(1, eax, ebx, ecx, edx); + registers.Leaf1Ecx = ecx; + registers.Leaf1Edx = edx; + } + if (basic >= 7) { + __cpuid_count(7, 0, eax, ebx, ecx, edx); + registers.Leaf7Ebx = ebx; + registers.Leaf7Ecx = ecx; + } + if (basic >= 0xD) { + __cpuid_count(0xD, 1, eax, ebx, ecx, edx); + registers.LeafDSub1Eax = eax; + } + const unsigned int extended = __get_cpuid_max(0x80000000, nullptr); + if (extended >= 0x80000001) { + __cpuid(0x80000001, eax, ebx, ecx, edx); + registers.Leaf80000001Ecx = ecx; + } + if (extended >= 0x80000008) { + __cpuid(0x80000008, eax, ebx, ecx, edx); + registers.Leaf80000008Ebx = ebx; + } + if ((registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0) { + std::uint32_t low = 0, high = 0; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + registers.Xcr0 = static_cast(high) << 32 | low; + } +#endif + return registers; +} + +std::string HostCpuModel() { +#if defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; + unsigned int brand[12] = {}; + for (unsigned int leaf = 0; leaf < 3; ++leaf) + __get_cpuid(0x80000002 + leaf, &brand[leaf * 4], &brand[leaf * 4 + 1], &brand[leaf * 4 + 2], &brand[leaf * 4 + 3]); + std::string model(reinterpret_cast(brand), strnlen(reinterpret_cast(brand), sizeof(brand))); + model.erase(0, model.find_first_not_of(' ')); + return model; +#else + return {}; +#endif +} + +std::vector HostCpuFeatures(const HostCpuRegisters& registers) { + std::vector features; + features.reserve(std::size(kFeatures) + 1); + for (const auto& feature : kFeatures) + features.push_back({feature.Name, feature.Required, (Value(registers, feature.Source) >> feature.Bit & 1) != 0}); + features.push_back({"OS AVX state (XCR0)", true, AvxStateEnabled(registers)}); + return features; +} + +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers) { + std::vector missing; + for (const auto& feature : HostCpuFeatures(registers)) + if (feature.Required && !feature.Present) missing.emplace_back(feature.Name); + return missing; +} + +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model) { + const auto missing = MissingHostCpuFeatures(registers); + if (missing.empty()) return; + throw std::runtime_error("The host CPU (" + DisplayModel(model) + ") lacks " + Join(missing) + + ". The PS5's Zen 2 CPU has these extensions and titles run their instructions natively, so this CPU cannot run them."); +} + +#if (defined(__x86_64__) || defined(__i386__)) && !defined(__APPLE__) +namespace { + +const bool HostCpuChecked = [] { + const auto registers = ReadHostCpuRegisters(); + const auto model = HostCpuModel(); + std::vector present; + std::vector absent; + for (const auto& feature : HostCpuFeatures(registers)) + (feature.Present ? present : absent).emplace_back(feature.Name); + APS5_LOG_OUT("host CPU %s: has %s; lacks %s", DisplayModel(model).c_str(), Join(present).c_str(), absent.empty() ? "none" : Join(absent).c_str()); + CheckHostCpu(registers, model); + return true; +}(); + +} +#endif diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c071fe1075..ba1525bda9 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -1,10 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/System/include/HostCpu.hpp" #include -#if defined(__x86_64__) || defined(__i386__) -#include -#endif #include #include #include @@ -49,20 +47,6 @@ bool IsLeafMib(const int* name) { return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); } -std::string HostCpuModel() { -#if defined(__x86_64__) || defined(__i386__) - if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; - unsigned int brand[12] = {}; - for (unsigned int leaf = 0; leaf < 3; ++leaf) - __get_cpuid(0x80000002 + leaf, &brand[leaf * 4], &brand[leaf * 4 + 1], &brand[leaf * 4 + 2], &brand[leaf * 4 + 3]); - std::string model(reinterpret_cast(brand), strnlen(reinterpret_cast(brand), sizeof(brand))); - model.erase(0, model.find_first_not_of(' ')); - return model; -#else - return {}; -#endif -} - } extern "C" { diff --git a/core/libs/tests/HostCpu.cpp b/core/libs/tests/HostCpu.cpp new file mode 100644 index 0000000000..7c5d3aea88 --- /dev/null +++ b/core/libs/tests/HostCpu.cpp @@ -0,0 +1,139 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +static constexpr std::uint32_t Bit(unsigned bit) { + return std::uint32_t{1} << bit; +} + +static HostCpuRegisters Zen2() { + HostCpuRegisters registers; + registers.Leaf1Ecx = Bit(0) | Bit(1) | Bit(9) | Bit(12) | Bit(13) | Bit(19) | Bit(20) | Bit(22) | Bit(23) | Bit(25) | Bit(26) | Bit(27) | Bit(28) | Bit(29) | Bit(30); + registers.Leaf1Edx = Bit(25) | Bit(26); + registers.Leaf7Ebx = Bit(3) | Bit(5) | Bit(8) | Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29); + registers.Leaf7Ecx = Bit(22); + registers.LeafDSub1Eax = Bit(0) | Bit(1); + registers.Leaf80000001Ecx = Bit(0) | Bit(5) | Bit(6) | Bit(8); + registers.Leaf80000008Ebx = Bit(0); + registers.Xcr0 = 0x7; + return registers; +} + +static std::string Text(const std::vector& names) { + std::string text; + for (const auto& name : names) text += (text.empty() ? "" : ",") + name; + return text; +} + +static void CheckMissing(const HostCpuRegisters& registers, const std::vector& expected, const std::string& label) { + const auto missing = MissingHostCpuFeatures(registers); + Require(missing == expected, label + ": expected [" + Text(expected) + "], got [" + Text(missing) + "]"); +} + +static void CheckZen2() { + CheckMissing(Zen2(), {}, "Zen 2"); + for (const auto& feature : HostCpuFeatures(Zen2())) + Require(feature.Present, std::string("Zen 2 lacks ") + feature.Name); +} + +static void CheckIntelWithoutAmdOnlyExtensions() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~(Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29)); + registers.Leaf7Ecx = 0; + registers.LeafDSub1Eax = Bit(0); + registers.Leaf80000001Ecx &= ~(Bit(6) | Bit(8)); + registers.Leaf80000008Ebx = 0; + CheckMissing(registers, {}, "AVX2 host without SSE4A, CLZERO, SHA, ADX, RDSEED, CLFLUSHOPT, CLWB, RDPID"); +} + +static void CheckSingleMissingExtension() { + struct Case { + const char* Name; + std::uint32_t HostCpuRegisters::* Register; + unsigned Bit; + }; + const Case cases[] = { + {"BMI2", &HostCpuRegisters::Leaf7Ebx, 8}, + {"BMI1", &HostCpuRegisters::Leaf7Ebx, 3}, + {"AVX2", &HostCpuRegisters::Leaf7Ebx, 5}, + {"MOVBE", &HostCpuRegisters::Leaf1Ecx, 22}, + {"FMA", &HostCpuRegisters::Leaf1Ecx, 12}, + {"F16C", &HostCpuRegisters::Leaf1Ecx, 29}, + {"POPCNT", &HostCpuRegisters::Leaf1Ecx, 23}, + {"SSE4.2", &HostCpuRegisters::Leaf1Ecx, 20}, + {"CMPXCHG16B", &HostCpuRegisters::Leaf1Ecx, 13}, + {"LZCNT", &HostCpuRegisters::Leaf80000001Ecx, 5}, + {"LAHF/SAHF", &HostCpuRegisters::Leaf80000001Ecx, 0}, + }; + for (const auto& entry : cases) { + auto registers = Zen2(); + registers.*entry.Register &= ~Bit(entry.Bit); + CheckMissing(registers, {entry.Name}, std::string("without ") + entry.Name); + } +} + +static void CheckAvxWithoutOsState() { + auto registers = Zen2(); + registers.Xcr0 = 0x3; + CheckMissing(registers, {"OS AVX state (XCR0)"}, "XCR0 without YMM state"); + registers = Zen2(); + registers.Leaf1Ecx &= ~Bit(27); + CheckMissing(registers, {"OS AVX state (XCR0)"}, "OSXSAVE clear"); +} + +static void CheckPreHaswell() { + auto registers = Zen2(); + registers.Leaf1Ecx &= ~(Bit(12) | Bit(22)); + registers.Leaf7Ebx = 0; + registers.Leaf80000001Ecx &= ~Bit(5); + CheckMissing(registers, {"LZCNT", "BMI1", "BMI2", "MOVBE", "AVX2", "FMA"}, "AVX host without AVX2"); +} + +static void CheckError() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~Bit(8); + CheckHostCpu(Zen2(), "AMD Ryzen 7 3700X 8-Core Processor"); + try { + CheckHostCpu(registers, "Example CPU"); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(Example CPU) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, "AMD Example "); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(AMD Example) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, ""); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(unknown model) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } +} + +int main() { + try { + CheckZen2(); + CheckIntelWithoutAmdOnlyExtensions(); + CheckSingleMissingExtension(); + CheckAvxWithoutOsState(); + CheckPreHaswell(); + CheckError(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 709e78c6d4..b3b9adbe26 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -143,7 +143,8 @@ - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. -- [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host CPU's name, built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) +- [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) +- [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked, because Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE without reporting them through CPUID - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index e52f0383bf..aa201d4ac5 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -37,6 +37,8 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +Titles run natively, so the host CPU needs the extensions of the PS5's Zen 2 CPU that compiled code uses without checking, among them AVX2, FMA, F16C, BMI1, BMI2, LZCNT and MOVBE. On Linux and Windows a converted title logs the host CPU's extensions at start-up and stops before running any title code, naming the CPU and the missing extensions, if one is absent ([list](../dev/debt/libkernel.md)). + The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options From 4a0489a7a914af287c6ad3345896dfa46e95c8db Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Wed, 14 Oct 2026 10:36:22 +0400 Subject: [PATCH 321/353] fix(shader): map shader cache files on Windows --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 22 +++++++-- core/shader/recompiler/ShaderDiskCache.cpp | 47 ++++++++++++++----- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 184c778f5e..447fdb8cb6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -600,17 +600,29 @@ void verifyPersist() { const auto key = sample.Key(); const auto variant = sampleVariant(); const auto persisted = ShaderDiskCache::Persist(key, variant); -#ifdef _WIN32 - require(persisted == nullptr, "an entry was persisted where the cache does not map entries"); -#else require(persisted != nullptr, "a persisted entry was not read back"); - requireSameVariant(variant, *persisted, "persist round trip"); const auto path = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "persisted SPIR-V is not a mapped cache entry"); + const auto close = [](void* handle) { CloseHandle(handle); }; + std::unique_ptr writer(CreateFileW(path.c_str(), GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr), close); + require(writer.get() != INVALID_HANDLE_VALUE, "cannot retain the published cache writer"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "cannot load a cache entry while its writer retains rename access"); + const auto& loadedWords = loaded.artifact.spirv; + require(VirtualQuery(loadedWords.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "cache loading with a retained writer did not map the entry"); + writer.reset(); +#endif + requireSameVariant(variant, *persisted, "persist round trip"); + auto detached = persisted->artifact.spirv; + detached[0] ^= 1u; + require(detached[0] != persisted->artifact.spirv[0], "changing a copied mapped module changed its owner"); require(WriteFileAtomically(path, std::vector(64, std::byte{0x5a})), "cannot replace the persisted entry"); std::error_code error; std::filesystem::remove(path, error); + require(!error, "cannot remove the replaced cache entry"); requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); -#endif } struct ComputeRequest { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 36d4e68724..c650f519c2 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -170,13 +170,18 @@ std::size_t PayloadOffset(std::size_t keyBytes) { return (sizeof(FileHeader) + keyBytes + alignment - 1) / alignment * alignment; } -#ifndef _WIN32 class MappedFile { public: MappedFile(const void* data, std::size_t size) : data(data), size(size) {} MappedFile(const MappedFile&) = delete; MappedFile& operator=(const MappedFile&) = delete; - ~MappedFile() { munmap(const_cast(data), size); } + ~MappedFile() { +#ifdef _WIN32 + UnmapViewOfFile(data); +#else + munmap(const_cast(data), size); +#endif + } [[nodiscard]] std::span Bytes() const { return {static_cast(data), size}; } @@ -186,6 +191,27 @@ private: }; std::shared_ptr MapFile(const std::filesystem::path& path) { +#ifdef _WIN32 + const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return nullptr; + LARGE_INTEGER length{}; + if (!GetFileSizeEx(file, &length) || length.QuadPart <= 0 || static_cast(length.QuadPart) > std::numeric_limits::max()) { + CloseHandle(file); + return nullptr; + } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (mapping == nullptr) return nullptr; + const void* data = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (data == nullptr) return nullptr; + try { + return std::make_shared(data, static_cast(length.QuadPart)); + } catch (...) { + UnmapViewOfFile(data); + throw; + } +#else const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC); if (descriptor < 0) return nullptr; struct stat status{}; @@ -193,9 +219,14 @@ std::shared_ptr MapFile(const std::filesystem::path& path) { if (fstat(descriptor, &status) == 0 && status.st_size > 0) data = mmap(nullptr, static_cast(status.st_size), PROT_READ, MAP_PRIVATE, descriptor, 0); close(descriptor); if (data == MAP_FAILED) return nullptr; - return std::make_shared(data, static_cast(status.st_size)); -} + try { + return std::make_shared(data, static_cast(status.st_size)); + } catch (...) { + munmap(data, static_cast(status.st_size)); + throw; + } #endif +} static_assert(sizeof(FileHeader) == 48 && std::is_trivially_copyable_v); class Writer { @@ -1220,11 +1251,6 @@ public: } std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { -#ifdef _WIN32 - static_cast(key); - static_cast(variant); - return nullptr; -#else const auto path = directory / EntryName(key); const auto file = EncodeEntry(key, variant); if (!WriteFileAtomically(path, file)) return nullptr; @@ -1238,7 +1264,6 @@ public: persisted->layout = variant.layout; persisted->artifact.variantId = variant.artifact.variantId; return persisted; -#endif } void Flush() { @@ -1278,12 +1303,10 @@ private: bool loaded = false; std::span bytes; std::shared_ptr owner; -#ifndef _WIN32 if (auto mapped = MapFile(directory / name)) { bytes = mapped->Bytes(); owner = std::move(mapped); } -#endif const bool present = owner != nullptr || ReadWholeFile(directory / name, file); if (owner == nullptr) bytes = file; if (!present) { From 6d35b92ba68bcb20c699abf0412ea0c7610799d7 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 14 Oct 2026 10:48:45 +0300 Subject: [PATCH 322/353] feat(ci): truncate long release commits list --- .github/workflows/release.yml | 7 ++++- tools/prepare_release_notes.py | 43 +++++++++++++++++++++++++++++ tools/tests/test_package_release.py | 37 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tools/prepare_release_notes.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbab5a8f97..cb2dde5d4d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,12 @@ jobs: done python tools/package_release.py --docs docs/user --output dist chmod +x "dist/relinker-$RELEASE_TAG" + - name: Prepare release notes + shell: bash + run: | + gh api --method POST "repos/$GH_REPO/releases/generate-notes" -f tag_name="$RELEASE_TAG" > "$RUNNER_TEMP/release-notes.json" + python tools/prepare_release_notes.py --input "$RUNNER_TEMP/release-notes.json" --body "$RUNNER_TEMP/release-body.md" --full dist/release-notes.md - name: Create draft and upload files - run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes + run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-body.md" - name: Publish complete release run: gh release edit "$RELEASE_TAG" --draft=false diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py new file mode 100644 index 0000000000..fcff9c4ddc --- /dev/null +++ b/tools/prepare_release_notes.py @@ -0,0 +1,43 @@ +import argparse +import json +import re +from pathlib import Path + +# GitHub's release body limit is measured in characters. +BODY_LIMIT = 125000 + + +def release_body(notes): + if len(notes) <= BODY_LIMIT: + return notes + # Keep the next section and the changelog link, even without contributors. + return re.sub( + r"^## What's Changed[^\S\r\n]*\r?\n.*?(?=^#{1,2} |^\*\*Full Changelog\*\*:|\Z)", + "", + notes, + count=1, + flags=re.MULTILINE | re.DOTALL, + ) + + +def prepare_notes(source, body, full): + notes = json.loads(source.read_text(encoding="utf-8"))["body"] + if not isinstance(notes, str): + raise ValueError("Generated release notes body must be a string") + full.parent.mkdir(parents=True, exist_ok=True) + full.write_text(notes, encoding="utf-8", newline="") + # Do not shorten anything else: GitHub should reject an oversized remainder. + body.write_text(release_body(notes), encoding="utf-8", newline="") + + +def main(): + parser = argparse.ArgumentParser(description="Prepare size-safe GitHub release notes") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--body", type=Path, required=True) + parser.add_argument("--full", type=Path, required=True) + args = parser.parse_args() + prepare_notes(args.input, args.body, args.full) + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index db1a62b752..ce95a7bcd0 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -1,4 +1,5 @@ import os +import json import sys import tarfile import tempfile @@ -10,6 +11,7 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import package_release +import prepare_release_notes @contextmanager @@ -240,5 +242,40 @@ class ReleaseAssetsTests(unittest.TestCase): self.assertEqual((output / name).read_bytes(), b"old " + name.encode()) +class ReleaseNotesTests(unittest.TestCase): + def test_short_notes_are_unchanged_including_unicode_at_limit(self): + for notes in ("", "## What's Changed\n* Fix\n", "я" * 125000): + self.assertEqual(prepare_release_notes.release_body(notes), notes) + + def test_only_changes_section_is_removed_from_long_notes(self): + for newline in ("\n", "\r\n"): + for tail in ("## New Contributors\n* @example\n\n**Full Changelog**: URL\n", + "**Full Changelog**: URL\n", ""): + with self.subTest(newline=newline, tail=tail): + prefix = "Release introduction" + newline + tail = tail.replace("\n", newline) + notes = prefix + "## What's Changed" + newline + "* change\n" * 20000 + tail + self.assertEqual(prepare_release_notes.release_body(notes), prefix + tail) + + def test_oversized_remainder_is_not_shortened(self): + remainder = "## New Contributors\n" + "x" * 125001 + notes = "## What's Changed\n* Fix\n\n" + remainder + self.assertEqual(prepare_release_notes.release_body(notes), remainder) + self.assertEqual(prepare_release_notes.release_body(remainder), remainder) + + def test_full_notes_are_preserved_in_asset(self): + for notes in ("", "## What's Changed\n* Fix\n", "я" * 125000, + "## What's Changed\n" + "* Fix\n" * 25000 + "## New Contributors\n* @example\n"): + with self.subTest(length=len(notes)), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "notes.json" + body = root / "body.md" + full = root / "dist/release-notes.md" + source.write_text(json.dumps({"body": notes}), encoding="utf-8") + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(full.read_bytes(), notes.encode("utf-8")) + self.assertEqual(body.read_bytes(), prepare_release_notes.release_body(notes).encode("utf-8")) + + if __name__ == "__main__": unittest.main() From 435be9905d11e2d7fe0cf856aeaaf684a47fb068 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Mon, 12 Oct 2026 21:52:06 +0100 Subject: [PATCH 323/353] feat(libSceIme): open a panel-less IME fed by the host keyboard Co-Authored-By: Claude Opus 5.5 --- core/libs/SceTypes.hpp | 2 +- core/libs/prx/libSceIme/CMakeLists.txt | 11 + core/libs/prx/libSceIme/Export.cpp | 359 ++++++++++++++++-- core/libs/prx/libSceIme/include/ImeInput.hpp | 26 ++ core/libs/prx/libSceVideoOut/CMakeLists.txt | 3 +- .../libSceVideoOut/include/ImeTextInput.hpp | 11 + .../prx/libSceVideoOut/src/ImeTextInput.cpp | 42 ++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 + .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 3 + core/libs/tests/GuestImeOpen.cpp | 350 +++++++++++++++++ docs/dev/debt/libSceIme.md | 7 +- 11 files changed, 789 insertions(+), 27 deletions(-) create mode 100644 core/libs/prx/libSceIme/include/ImeInput.hpp create mode 100644 core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp create mode 100644 core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp create mode 100644 core/libs/tests/GuestImeOpen.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3f73232949..9ddb8391f4 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1181,7 +1181,7 @@ struct ImeEvent { ImeEventParam param; }; -using EventHandler = void (*)(void* arg, const ImeEvent* event); +using EventHandler = void (APS5_VABI *)(void* arg, const ImeEvent* event); struct Param { std::int32_t user_id; diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 489c43cc1a..5f45b0907c 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -22,4 +22,15 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_keyboard PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_ime_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeOpen.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) + target_include_directories(guest_ime_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(guest_ime_open_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_open_tests) + add_test(NAME guest_ime_open COMMAND guest_ime_open_tests) + if(WIN32) + set_tests_properties(guest_ime_open PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 4675c980f5..d686445211 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,52 +1,346 @@ +#include #include #include #include +#include #include +#include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" namespace { +constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); +constexpr int ErrorInvalidText = static_cast(0x80bc0006u); constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); +constexpr int ErrorInvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int ErrorInvalidInputMethod = static_cast(0x80bc0014u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); -constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidPosX = static_cast(0x80bc0018u); +constexpr int ErrorInvalidPosY = static_cast(0x80bc0019u); +constexpr int ErrorInvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int ErrorInvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int ErrorInvalidExtended = static_cast(0x80bc001cu); +constexpr int ErrorInvalidWork = static_cast(0x80bc0020u); +constexpr int ErrorInvalidHandler = static_cast(0x80bc0022u); constexpr int ErrorNoResourceId = static_cast(0x80bc0023u); constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); +constexpr int ErrorInvalidParam = static_cast(0x80bc0030u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInvalidReserved = static_cast(0x80bc0032u); constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; +constexpr uint32_t OptionMultiline = 0x00000001; constexpr uint32_t OptionUseOver2K = 0x00004000; +constexpr uint32_t EnterLabelLast = 3; +constexpr uint32_t AlignmentLast = 2; +constexpr uint32_t PanelPriorityLast = 3; +constexpr uint32_t MaxTextLength = 2048; constexpr int32_t UserIdInvalid = -1; +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; +constexpr uint32_t CaretMoveLeft = 1; +constexpr uint32_t CaretMoveRight = 2; + +struct Session { + Param param{}; + std::u16string text; + std::uint32_t caret = 0; + std::deque events; +}; + std::mutex g_keyboardMutex; std::set g_openKeyboards; +std::mutex g_imeMutex; +std::optional g_session; + +void PanelSize(const Param& param, uint32_t* width, uint32_t* height) { + const uint32_t scale = (param.option & OptionUseOver2K) != 0 ? 2 : 1; + *width = (param.type == TypeNumber ? 370u : 793u) * scale; + *height = (param.type == TypeNumber ? 402u : 408u) * scale; +} + +bool IsHighSurrogate(char16_t unit) { return unit >= 0xd800 && unit <= 0xdbff; } +bool IsLowSurrogate(char16_t unit) { return unit >= 0xdc00 && unit <= 0xdfff; } + +bool ValidText(const char16_t* text, std::size_t length, bool multiline) { + for (std::size_t i = 0; i < length; ++i) { + const char16_t unit = text[i]; + if (!multiline && (unit == u'\n' || unit == u'\r')) return false; + if (IsLowSurrogate(unit)) return false; + if (IsHighSurrogate(unit)) { + if (i + 1 >= length || !IsLowSurrogate(text[i + 1])) return false; + ++i; + } + } + return true; +} + +std::size_t TextLength(const char16_t* text, std::size_t limit) { + std::size_t length = 0; + while (length < limit && text[length] != u'\0') ++length; + return length; +} + +std::u16string DecodeUtf8(const char* text, std::size_t size) { + std::u16string units; + for (std::size_t i = 0; i < size;) { + const auto lead = static_cast(text[i]); + std::uint32_t codePoint = 0; + std::size_t extra = 0; + if (lead < 0x80) codePoint = lead; + else if ((lead & 0xe0) == 0xc0) { codePoint = lead & 0x1f; extra = 1; } + else if ((lead & 0xf0) == 0xe0) { codePoint = lead & 0x0f; extra = 2; } + else if ((lead & 0xf8) == 0xf0) { codePoint = lead & 0x07; extra = 3; } + else throw std::invalid_argument("IME host input: invalid UTF-8 lead byte"); + if (extra != 0 && i + extra >= size) throw std::invalid_argument("IME host input: truncated UTF-8 sequence"); + for (std::size_t k = 1; k <= extra; ++k) { + const auto next = static_cast(text[i + k]); + if ((next & 0xc0) != 0x80) throw std::invalid_argument("IME host input: invalid UTF-8 continuation byte"); + codePoint = (codePoint << 6) | (next & 0x3f); + } + i += extra + 1; + if (codePoint >= 0x10000) { + codePoint -= 0x10000; + units.push_back(static_cast(0xd800 + (codePoint >> 10))); + units.push_back(static_cast(0xdc00 + (codePoint & 0x3ff))); + } else { + units.push_back(static_cast(codePoint)); + } + } + return units; +} + +void SyncBuffers(Session& session) { + const std::size_t bytes = session.text.size() * sizeof(char16_t); + std::memcpy(session.param.input_text_buffer, session.text.data(), bytes); + session.param.input_text_buffer[session.text.size()] = u'\0'; + auto* work = static_cast(session.param.work); + std::memcpy(work, session.text.data(), bytes); + work[session.text.size()] = u'\0'; +} + +void QueueText(Session& session, uint32_t id, uint32_t index, int32_t delta) { + ImeEvent event{}; + event.id = id; + event.param.text.str = static_cast(session.param.work); + event.param.text.caret_index = session.caret; + event.param.text.area_num = 1; + event.param.text.text_area[0].mode = TextAreaMode::Edit; + event.param.text.text_area[0].index = index; + event.param.text.text_area[0].length = delta; + session.events.push_back(event); +} + +void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { + for (std::size_t i = 0; i < steps; ++i) { + ImeEvent event{}; + event.id = EventUpdateCaret; + event.param.caret_move = direction; + session.events.push_back(event); + } +} + +void Insert(Session& session, std::u16string units) { + const bool multiline = (session.param.option & OptionMultiline) != 0; + std::u16string accepted; + for (const char16_t unit : units) { + if (!multiline && (unit == u'\n' || unit == u'\r')) continue; + accepted.push_back(unit); + } + const std::size_t room = session.param.max_text_length - session.text.size(); + std::size_t count = std::min(accepted.size(), room); + if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; + if (count == 0) return; + const uint32_t index = session.caret; + session.text.insert(index, accepted, 0, count); + session.caret += static_cast(count); + SyncBuffers(session); + QueueText(session, EventUpdateText, index, static_cast(count)); +} + +std::size_t UnitsBefore(const Session& session) { + const uint32_t caret = session.caret; + if (caret == 0) return 0; + return caret >= 2 && IsLowSurrogate(session.text[caret - 1]) && IsHighSurrogate(session.text[caret - 2]) ? 2 : 1; +} + +std::size_t UnitsAfter(const Session& session) { + const uint32_t caret = session.caret; + if (caret >= session.text.size()) return 0; + return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; +} + +void Apply(Session& session, const ImeHostInput& input) { + switch (input.kind) { + case ImeHostInputKind::Text: { + std::size_t size = 0; + while (size < sizeof(input.text) && input.text[size] != '\0') ++size; + Insert(session, DecodeUtf8(input.text, size)); + return; + } + case ImeHostInputKind::Backspace: { + const std::size_t units = UnitsBefore(session); + if (units == 0) return; + session.caret -= static_cast(units); + session.text.erase(session.caret, units); + SyncBuffers(session); + QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + return; + } + case ImeHostInputKind::Delete: { + const std::size_t units = UnitsAfter(session); + if (units == 0) return; + session.text.erase(session.caret, units); + SyncBuffers(session); + QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + return; + } + case ImeHostInputKind::Left: { + const std::size_t units = UnitsBefore(session); + session.caret -= static_cast(units); + QueueCaretSteps(session, CaretMoveLeft, units); + return; + } + case ImeHostInputKind::Right: { + const std::size_t units = UnitsAfter(session); + session.caret += static_cast(units); + QueueCaretSteps(session, CaretMoveRight, units); + return; + } + case ImeHostInputKind::Home: { + const std::size_t units = session.caret; + session.caret = 0; + QueueCaretSteps(session, CaretMoveLeft, units); + return; + } + case ImeHostInputKind::End: { + const std::size_t units = session.text.size() - session.caret; + session.caret = static_cast(session.text.size()); + QueueCaretSteps(session, CaretMoveRight, units); + return; + } + case ImeHostInputKind::Enter: + if ((session.param.option & OptionMultiline) != 0) { + Insert(session, u"\n"); + } else { + QueueText(session, EventPressEnter, session.caret, 0); + } + return; + case ImeHostInputKind::Escape: + QueueText(session, EventPressClose, session.caret, 0); + return; + } + throw std::invalid_argument("IME host input: unknown input kind"); +} + +int ValidateOpen(const Param* param, const ExtendedParam* extended) { + if (!param) return ErrorInvalidAddress; + if (param->user_id == UserIdInvalid) return ErrorInvalidUserId; + if (param->type > TypeNumber) return ErrorInvalidType; + if (param->enter_label > EnterLabelLast) return ErrorInvalidEnterLabel; + if (param->input_method != 0) return ErrorInvalidInputMethod; + if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; + if (param->max_text_length == 0 || param->max_text_length > MaxTextLength) return ErrorInvalidMaxTextLength; + if (!param->input_text_buffer) return ErrorInvalidInputTextBuffer; + const float maxX = (param->option & OptionUseOver2K) != 0 ? 3840.0f : 1920.0f; + const float maxY = (param->option & OptionUseOver2K) != 0 ? 2160.0f : 1080.0f; + if (!(param->posx >= 0.0f && param->posx < maxX)) return ErrorInvalidPosX; + if (!(param->posy >= 0.0f && param->posy < maxY)) return ErrorInvalidPosY; + if (param->horizontal_alignment > AlignmentLast) return ErrorInvalidHorizontalAlignment; + if (param->vertical_alignment > AlignmentLast) return ErrorInvalidVerticalAlignment; + if (extended) { + if (extended->priority > PanelPriorityLast) return ErrorInvalidExtended; + for (const std::int8_t byte : extended->reserved) { + if (byte != 0) return ErrorInvalidExtended; + } + } + if (!param->work || (reinterpret_cast(param->work) & 3u) != 0) return ErrorInvalidWork; + if (!param->handler) return ErrorInvalidHandler; + for (const std::int8_t byte : param->reserved) { + if (byte != 0) return ErrorInvalidReserved; + } + const std::size_t length = TextLength(param->input_text_buffer, param->max_text_length); + if (!ValidText(param->input_text_buffer, length, (param->option & OptionMultiline) != 0)) return ErrorInvalidText; + return 0; +} } extern "C" { +void ImePublishInput_nid_postfix(const ImeHostInput& input) { + std::lock_guard lock(g_imeMutex); + if (g_session) Apply(*g_session, input); +} + +bool ImeCapturesKeyboard_nid_postfix() { + std::lock_guard lock(g_imeMutex); + return g_session.has_value(); +} + +int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { + std::lock_guard lock(g_imeMutex); + if (g_session) return ErrorBusy; + if (const int error = ValidateOpen(param, extended)) return error; + if (param->filter) NotImplemented_nid_no_patch("sceImeOpen: text filter"); + if (extended && extended->ext_keyboard_filter) NotImplemented_nid_no_patch("sceImeOpen: external keyboard filter"); + Session session; + session.param = *param; + session.text.assign(param->input_text_buffer, TextLength(param->input_text_buffer, param->max_text_length)); + session.caret = static_cast(session.text.size()); + SyncBuffers(session); + ImeEvent open{}; + open.id = EventOpen; + open.param.rect.x = param->posx; + open.param.rect.y = param->posy; + PanelSize(*param, &open.param.rect.width, &open.param.rect.height); + session.events.push_back(open); + g_session = std::move(session); + return 0; +} + int APS5_VABI sceImeClose_nid_postfix(void) { - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + g_session.reset(); + return 0; } int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form) { - (void)form; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!form) return ErrorInvalidAddress; + const Param& param = g_session->param; + *form = {}; + form->type = param.type; + form->posx = param.posx; + form->posy = param.posy; + form->horizontal_alignment = param.horizontal_alignment; + form->vertical_alignment = param.vertical_alignment; + PanelSize(param, &form->width, &form->height); + return 0; } int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) { if (!param || !width || !height) return ErrorInvalidAddress; if (param->type > TypeNumber) return ErrorInvalidType; if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; - const uint32_t scale = (param->option & OptionUseOver2K) != 0 ? 2 : 1; - *width = (param->type == TypeNumber ? 370u : 793u) * scale; - *height = (param->type == TypeNumber ? 402u : 408u) * scale; + PanelSize(*param, width, height); return 0; } @@ -86,13 +380,6 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } -int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { - (void)param; - (void)extended; - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI sceImeParamInit(Param* param) { if (!param) return; std::memset(param, 0, sizeof(*param)); @@ -100,25 +387,49 @@ void APS5_VABI sceImeParamInit(Param* param) { } int APS5_VABI sceImeSetCaret(const Caret* caret) { - (void)caret; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!caret) return ErrorInvalidAddress; + if (caret->index > g_session->text.size()) return ErrorInvalidParam; + g_session->caret = caret->index; + return 0; } int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { - (void)text; - (void)length; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!text) return ErrorInvalidAddress; + Session& session = *g_session; + const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); + if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; + session.text.assign(text, count); + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + SyncBuffers(session); + return 0; } int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry) { - (void)mode; - (void)geometry; - return ErrorNotOpened; + (void)mode; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!geometry) return ErrorInvalidAddress; + return 0; } int APS5_VABI sceImeUpdate(EventHandler handler) { - if (!handler) APS5_INVALID_ARG_EX; - return 0; + if (!handler) APS5_INVALID_ARG_EX; + for (;;) { + ImeEvent event{}; + void* arg = nullptr; + { + std::lock_guard lock(g_imeMutex); + if (!g_session || g_session->param.handler != handler || g_session->events.empty()) return 0; + event = g_session->events.front(); + g_session->events.pop_front(); + arg = g_session->param.arg; + } + handler(arg, &event); + } } } diff --git a/core/libs/prx/libSceIme/include/ImeInput.hpp b/core/libs/prx/libSceIme/include/ImeInput.hpp new file mode 100644 index 0000000000..4212dd61ea --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP + +#include + +enum class ImeHostInputKind : std::uint32_t { + Text, + Backspace, + Delete, + Left, + Right, + Home, + End, + Enter, + Escape, +}; + +struct ImeHostInput { + ImeHostInputKind kind = ImeHostInputKind::Text; + char text[32] = {}; +}; + +extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); +extern "C" bool ImeCapturesKeyboard_nid_postfix(); + +#endif diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 13f9582b20..0bfa4cdd06 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ImeTextInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardMouse.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp @@ -18,7 +19,7 @@ target_include_directories(libSceVideoOut PRIVATE ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libc libkernel decoder_png SDL2-static) +target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32) endif() diff --git a/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp new file mode 100644 index 0000000000..81cfe59fc1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP + +#include "SDL_events.h" + +class ImeTextInput { +public: + void HandleEvent(const SDL_Event& event, unsigned windowId); +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp new file mode 100644 index 0000000000..26ddb4b216 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp @@ -0,0 +1,42 @@ +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" + +#include + +#include "SDL_keyboard.h" + +void ImeTextInput::HandleEvent(const SDL_Event& event, unsigned windowId) { + ImeHostInput input{}; + switch (event.type) { + case SDL_TEXTINPUT: + if (event.text.windowID != windowId) return; + static_assert(sizeof(input.text) >= sizeof(event.text.text)); + std::memcpy(input.text, event.text.text, sizeof(event.text.text)); + input.kind = ImeHostInputKind::Text; + break; + case SDL_KEYDOWN: + if (event.key.windowID != windowId) return; + switch (event.key.keysym.scancode) { + case SDL_SCANCODE_BACKSPACE: input.kind = ImeHostInputKind::Backspace; break; + case SDL_SCANCODE_DELETE: input.kind = ImeHostInputKind::Delete; break; + case SDL_SCANCODE_LEFT: input.kind = ImeHostInputKind::Left; break; + case SDL_SCANCODE_RIGHT: input.kind = ImeHostInputKind::Right; break; + case SDL_SCANCODE_HOME: input.kind = ImeHostInputKind::Home; break; + case SDL_SCANCODE_END: input.kind = ImeHostInputKind::End; break; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Enter; + break; + case SDL_SCANCODE_ESCAPE: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Escape; + break; + default: return; + } + break; + default: + return; + } + ImePublishInput_nid_postfix(input); +} diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index aff446317a..e5844a4b42 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -13,6 +13,7 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { } @@ -220,6 +221,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { const bool input = event.type == SDL_MOUSEWHEEL || event.type == SDL_MOUSEMOTION || event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!input || ignoreInput) return; + 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.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index a501eb12b7..2d9757c62c 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -14,6 +14,7 @@ #include "prx/libSceVideoOut/include/MouseInput.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceVideoOut/include/KeyboardInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -597,6 +598,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star PadInput padInput; MouseInput mouseInput; KeyboardInput keyboardInput; + ImeTextInput imeTextInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); @@ -619,6 +621,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (window.Handle() != nullptr) { mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); + imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } padInput.Update(); diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp new file mode 100644 index 0000000000..15029e736b --- /dev/null +++ b/core/libs/tests/GuestImeOpen.cpp @@ -0,0 +1,350 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended); +int APS5_VABI sceImeClose_nid_postfix(void); +int APS5_VABI sceImeUpdate(EventHandler handler); +int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); +int APS5_VABI sceImeSetCaret(const Caret* caret); +int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form); +void APS5_VABI sceImeParamInit(Param* param); +} + +namespace { + +constexpr int Busy = static_cast(0x80bc0001u); +constexpr int NotOpened = static_cast(0x80bc0002u); +constexpr int InvalidText = static_cast(0x80bc0006u); +constexpr int InvalidUserId = static_cast(0x80bc0010u); +constexpr int InvalidType = static_cast(0x80bc0011u); +constexpr int InvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int InvalidInputMethod = static_cast(0x80bc0014u); +constexpr int InvalidOption = static_cast(0x80bc0015u); +constexpr int InvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int InvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int InvalidPosX = static_cast(0x80bc0018u); +constexpr int InvalidPosY = static_cast(0x80bc0019u); +constexpr int InvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int InvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int InvalidExtended = static_cast(0x80bc001cu); +constexpr int InvalidWork = static_cast(0x80bc0020u); +constexpr int InvalidHandler = static_cast(0x80bc0022u); +constexpr int InvalidParam = static_cast(0x80bc0030u); +constexpr int InvalidAddress = static_cast(0x80bc0031u); +constexpr int InvalidReserved = static_cast(0x80bc0032u); + +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; + +struct Recorded { + uint32_t id; + void* arg; + ImeEventParam param; + std::u16string text; +}; + +std::vector g_events; +bool g_closeOnPressClose = false; +int g_reentrantClose = 1; + +void APS5_VABI Record(void* arg, const ImeEvent* event) { + Recorded recorded{event->id, arg, event->param, {}}; + if (event->id == EventUpdateText || event->id == EventPressEnter || event->id == EventPressClose) { + recorded.text = event->param.text.str; + } + g_events.push_back(recorded); + if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); +} + +void APS5_VABI Other(void*, const ImeEvent*) { + std::abort(); +} + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + int arg = 0; +}; + +Param MakeParam(Buffers& buffers, uint32_t maxLength) { + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = maxLength; + param.input_text_buffer = buffers.text; + param.posx = 100.0f; + param.posy = 200.0f; + param.work = buffers.work; + param.arg = &buffers.arg; + param.handler = Record; + return param; +} + +std::vector Drain() { + g_events.clear(); + Require(sceImeUpdate(Record) == 0, "update failed"); + return g_events; +} + +void Publish(ImeHostInputKind kind, const char* text = "") { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + ImePublishInput_nid_postfix(input); +} + +void RequireText(const Recorded& event, uint32_t id, const std::u16string& text, uint32_t caret, uint32_t index, int32_t delta) { + Require(event.id == id, "event id"); + Require(event.text == text, "event text"); + Require(event.param.text.caret_index == caret, "event caret"); + Require(event.param.text.area_num == 1, "event area count"); + Require(event.param.text.text_area[0].mode == TextAreaMode::Edit, "event area mode"); + Require(event.param.text.text_area[0].index == index, "event edit index"); + Require(event.param.text.text_area[0].length == delta, "event edit length"); +} + +void RequireCaretSteps(const std::vector& events, uint32_t direction, std::size_t count) { + Require(events.size() == count, "caret step count"); + for (const auto& event : events) { + Require(event.id == EventUpdateCaret && event.param.caret_move == direction, "caret step"); + } +} + +void CheckValidation() { + Buffers buffers; + Require(sceImeOpen_nid_postfix(nullptr, nullptr) == InvalidAddress, "null param"); + auto expect = [&](auto change, int error, const char* message) { + Param param = MakeParam(buffers, 16); + change(param); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == error, message); + }; + expect([](Param& p) { p.user_id = -1; }, InvalidUserId, "invalid user"); + expect([](Param& p) { p.type = 5; }, InvalidType, "invalid type"); + expect([](Param& p) { p.enter_label = 4; }, InvalidEnterLabel, "invalid enter label"); + expect([](Param& p) { p.input_method = 1; }, InvalidInputMethod, "invalid input method"); + expect([](Param& p) { p.option = 0x80000000u; }, InvalidOption, "invalid option"); + expect([](Param& p) { p.max_text_length = 0; }, InvalidMaxTextLength, "zero length"); + expect([](Param& p) { p.max_text_length = 2049; }, InvalidMaxTextLength, "long length"); + expect([](Param& p) { p.input_text_buffer = nullptr; }, InvalidInputTextBuffer, "null buffer"); + expect([](Param& p) { p.posx = -1.0f; }, InvalidPosX, "negative x"); + expect([](Param& p) { p.posx = 1920.0f; }, InvalidPosX, "x past 1080p"); + expect([](Param& p) { p.posy = 1080.0f; }, InvalidPosY, "y past 1080p"); + expect([](Param& p) { p.horizontal_alignment = 3; }, InvalidHorizontalAlignment, "horizontal alignment"); + expect([](Param& p) { p.vertical_alignment = 3; }, InvalidVerticalAlignment, "vertical alignment"); + expect([](Param& p) { p.work = nullptr; }, InvalidWork, "null work"); + expect([&](Param& p) { p.work = buffers.work + 2; }, InvalidWork, "misaligned work"); + expect([](Param& p) { p.handler = nullptr; }, InvalidHandler, "null handler"); + expect([](Param& p) { p.reserved[7] = 1; }, InvalidReserved, "reserved bytes"); + buffers.text[0] = 0xdc00; + expect([](Param&) {}, InvalidText, "lone low surrogate"); + buffers.text[0] = u'\n'; + expect([](Param&) {}, InvalidText, "newline without multiline"); + buffers.text[0] = 0; + + Param param = MakeParam(buffers, 16); + ExtendedParam extended{}; + extended.priority = 4; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended priority"); + extended.priority = 0; + extended.reserved[59] = 1; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended reserved"); + + param.posx = 3000.0f; + param.option = 0x4000; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "over-2K coordinates"); + Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); + + param = MakeParam(buffers, 16); + param.filter = reinterpret_cast(&Record); + bool threw = false; + try { + sceImeOpen_nid_postfix(¶m, nullptr); + } catch (const std::exception&) { + threw = true; + } + Require(threw, "text filter must throw"); + Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); +} + +void CheckEditing() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured before open"); + Require(sceImeUpdate(Record) == 0 && g_events.empty(), "update before open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == Busy, "second open"); + Require(ImeCapturesKeyboard_nid_postfix(), "keyboard not captured"); + Require(sceImeUpdate(Other) == 0, "update with another handler"); + + auto events = Drain(); + Require(events.size() == 1 && events[0].id == EventOpen, "open event"); + Require(events[0].arg == &buffers.arg, "handler argument"); + Require(events[0].param.rect.x == 100.0f && events[0].param.rect.y == 200.0f, "open position"); + Require(events[0].param.rect.width == 793 && events[0].param.rect.height == 408, "open size"); + Require(Drain().empty(), "events delivered twice"); + + PositionAndForm form{}; + Require(sceImeGetPanelPositionAndForm(nullptr) == InvalidAddress, "null panel form"); + Require(sceImeGetPanelPositionAndForm(&form) == 0, "panel form"); + Require(form.posx == 100.0f && form.posy == 200.0f && form.width == 793 && form.height == 408, "panel form values"); + + Publish(ImeHostInputKind::Text, "c"); + events = Drain(); + Require(events.size() == 1, "one text event"); + RequireText(events[0], EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"abc", "work copy"); + + Publish(ImeHostInputKind::Text, "\xc3\xa9\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcé\U0001F600", 6, 3, 3); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 2); + Publish(ImeHostInputKind::Backspace); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc\U0001F600", 3, 3, -1); + Publish(ImeHostInputKind::Delete); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc", 3, 3, -2); + Publish(ImeHostInputKind::Delete); + Require(Drain().empty(), "delete at the end"); + Publish(ImeHostInputKind::Home); + RequireCaretSteps(Drain(), 1, 3); + Publish(ImeHostInputKind::Left); + Require(Drain().empty(), "left at the start"); + Publish(ImeHostInputKind::Backspace); + Require(Drain().empty(), "backspace at the start"); + Publish(ImeHostInputKind::Right); + RequireCaretSteps(Drain(), 2, 1); + Publish(ImeHostInputKind::End); + RequireCaretSteps(Drain(), 2, 2); + + Publish(ImeHostInputKind::Text, "x\ny"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy", 5, 3, 2); + Publish(ImeHostInputKind::Text, "12\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12", 7, 5, 2); + Publish(ImeHostInputKind::Text, "\xf0\x9f\x98\x80"); + Require(Drain().empty(), "a pair past the limit must not be split"); + Publish(ImeHostInputKind::Text, "z"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12z", 8, 7, 1); + Publish(ImeHostInputKind::Text, "q"); + Require(Drain().empty(), "text past the limit"); + + Require(sceImeSetText(nullptr, 1) == InvalidAddress, "null text"); + Require(sceImeSetText(u"a\nb", 3) == InvalidText, "newline in single-line text"); + Require(sceImeSetText(u"hello", 5) == 0, "set text"); + Require(std::u16string(buffers.text) == u"hello", "set text buffer"); + Require(Drain().empty(), "set text queued an event"); + Caret caret{}; + caret.index = 6; + Require(sceImeSetCaret(nullptr) == InvalidAddress, "null caret"); + Require(sceImeSetCaret(&caret) == InvalidParam, "caret past the text"); + caret.index = 1; + Require(sceImeSetCaret(&caret) == 0, "set caret"); + Publish(ImeHostInputKind::Text, "X"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"hXello", 2, 1, 1); + + Publish(ImeHostInputKind::Enter); + events = Drain(); + RequireText(events.at(0), EventPressEnter, u"hXello", 2, 2, 0); + g_closeOnPressClose = true; + Publish(ImeHostInputKind::Escape); + Publish(ImeHostInputKind::Enter); + events = Drain(); + g_closeOnPressClose = false; + Require(events.size() == 1, "events after a close inside the handler"); + RequireText(events[0], EventPressClose, u"hXello", 2, 2, 0); + Require(g_reentrantClose == 0, "close inside the handler"); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured after close"); + Require(sceImeClose_nid_postfix() == NotOpened, "second close"); + Publish(ImeHostInputKind::Text, "ignored"); + Require(std::u16string(buffers.text) == u"hXello", "input after close"); + Require(sceImeSetText(u"a", 1) == NotOpened, "set text after close"); +} + +void CheckMultiline() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + param.option = 1; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "multiline open"); + Drain(); + Publish(ImeHostInputKind::Text, "a"); + Publish(ImeHostInputKind::Enter); + Publish(ImeHostInputKind::Text, "b\nc"); + const auto events = Drain(); + Require(events.size() == 3, "multiline events"); + RequireText(events[1], EventUpdateText, u"a\nb\nc", 2, 1, 1); + RequireText(events[2], EventUpdateText, u"a\nb\nc", 5, 2, 3); + Require(sceImeSetText(u"x\ny", 3) == 0, "multiline set text"); + Require(sceImeClose_nid_postfix() == 0, "multiline close"); +} + +void CheckHostEvents() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "host events open"); + Drain(); + ImeTextInput input; + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = 7; + std::strcpy(event.text.text, "hi"); + input.HandleEvent(event, 7); + event.text.windowID = 8; + input.HandleEvent(event, 7); + event = {}; + event.type = SDL_KEYDOWN; + event.key.windowID = 7; + event.key.keysym.scancode = SDL_SCANCODE_BACKSPACE; + event.key.repeat = 1; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + input.HandleEvent(event, 7); + event.key.repeat = 0; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_A; + input.HandleEvent(event, 7); + event.type = SDL_KEYUP; + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + input.HandleEvent(event, 7); + const auto events = Drain(); + Require(events.size() == 3, "host event count"); + RequireText(events[0], EventUpdateText, u"h", 2, 0, 2); + RequireText(events[1], EventUpdateText, u"h", 1, 1, -1); + Require(events[2].id == EventPressEnter, "host enter"); + Require(sceImeClose_nid_postfix() == 0, "host events close"); +} + +} + +int main() { + CheckValidation(); + CheckEditing(); + CheckMultiline(); + CheckHostEvents(); +} diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 37286b5b12..8511d88e3d 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -1,8 +1,13 @@ # libSceIme technical debt +### Silent stubs + +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world + ### Unknown function info -- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title +- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title +- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn ### Functional From a48c00660eae7afd204fdb7513a52380dddf5a04 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 14 Oct 2026 10:52:14 +0300 Subject: [PATCH 324/353] refactor(ci): refactor release notes prepare py --- tools/prepare_release_notes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index fcff9c4ddc..ed4c2e5562 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -3,14 +3,12 @@ import json import re from pathlib import Path -# GitHub's release body limit is measured in characters. BODY_LIMIT = 125000 def release_body(notes): if len(notes) <= BODY_LIMIT: return notes - # Keep the next section and the changelog link, even without contributors. return re.sub( r"^## What's Changed[^\S\r\n]*\r?\n.*?(?=^#{1,2} |^\*\*Full Changelog\*\*:|\Z)", "", @@ -26,7 +24,6 @@ def prepare_notes(source, body, full): raise ValueError("Generated release notes body must be a string") full.parent.mkdir(parents=True, exist_ok=True) full.write_text(notes, encoding="utf-8", newline="") - # Do not shorten anything else: GitHub should reject an oversized remainder. body.write_text(release_body(notes), encoding="utf-8", newline="") From 8cc20401a2aef4d7cd1503b5d25fa35508f2dd6d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 21:11:38 +1300 Subject: [PATCH 325/353] fix(libc): forget a released section's pages even while views remain on Windows WindowsMappings keys each physical page's state (moved into the section, saved bytes of an unmapped resident page) by the section handle value. sceKernelReleaseDirectMemory frees the backing before it unmaps the views, so ForgetSection ran while those views still existed and kept their slots. After the views went, the slots stayed under a handle value that Windows hands out again, and the next direct allocation whose section got that value started with the old page state: its first mapping went to the section instead of private memory, or received the old allocation's bytes. Views now share ownership of their slot, ForgetSection drops every slot of the section at once, and JIT shared memory forgets its section before closing it too. --- .../libs/prx/libc/include/WindowsMappings.hpp | 41 +++++++++---------- core/libs/prx/libkernel/DirectMemory/Jit.cpp | 5 ++- core/libs/tests/WindowsWriteWatch.cpp | 31 ++++++++++++++ 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 87f9f78308..c2c04713d5 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -83,7 +83,11 @@ public: HANDLE duplicate = nullptr; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); - const auto slotOf = [&](std::size_t done) -> Physical& { return physical[std::make_pair(reinterpret_cast(section), offset + done)]; }; + const auto slotOf = [&](std::size_t done) -> const std::shared_ptr& { + auto& slot = physical[std::make_pair(reinterpret_cast(section), offset + done)]; + if (!slot) slot = std::make_shared(); + return slot; + }; const auto pageOf = [](Physical& slot) { auto shared = slot.page.lock(); if (!shared) { @@ -93,7 +97,7 @@ public: return shared; }; const auto single = [&](std::size_t done) { - const auto& slot = slotOf(done); + const auto& slot = *slotOf(done); const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; @@ -106,14 +110,14 @@ public: commitResident(base, runBytes); residentRuns.emplace(base, Run{base + runBytes, {}, static_cast(runBytes >> 12)}); for (std::size_t at = 0; at < runBytes; at += pageBytes) { - auto& slot = slotOf(done + at); - const auto shared = pageOf(slot); - if (slot.saved) { - std::memcpy(reinterpret_cast(base + at), slot.saved->bytes + slot.savedOffset, pageBytes); - slot.saved.reset(); + const auto slot = slotOf(done + at); + const auto shared = pageOf(*slot); + if (slot->saved) { + std::memcpy(reinterpret_cast(base + at), slot->saved->bytes + slot->savedOffset, pageBytes); + slot->saved.reset(); } shared->aliases.push_back(base + at); - views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, &slot}); + views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, slot}); invalidate(*shared); } remember(freshRanges, base, base + runBytes); @@ -122,17 +126,17 @@ public: done += runBytes; continue; } - auto& slot = slotOf(done); - const auto shared = pageOf(slot); + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); const auto aliases = shared->aliases; for (const auto alias : aliases) { if (views.at(alias).resident) demote(alias); } split(base, pageBytes); mapShared(base, section, offset + done, protection); - slot.stored = true; + slot->stored = true; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, &slot}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot}); remember(freshRanges, base, base + pageBytes); invalidate(*shared); done += pageBytes; @@ -209,14 +213,7 @@ public: void ForgetSection(HANDLE section) { std::lock_guard lock(mutex); const auto key = reinterpret_cast(section); - for (auto slot = physical.lower_bound(std::make_pair(key, std::uint64_t{0})); slot != physical.end() && slot->first.first == key;) { - const auto shared = slot->second.page.lock(); - if (shared && !shared->aliases.empty()) { - ++slot; - continue; - } - slot = physical.erase(slot); - } + physical.erase(physical.lower_bound(std::make_pair(key, std::uint64_t{0})), physical.lower_bound(std::make_pair(key + 1, std::uint64_t{0}))); } bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { @@ -513,7 +510,7 @@ private: std::uint32_t hostWrites; std::uint8_t collected = 0; bool resident = false; - Physical* slot = nullptr; + std::shared_ptr slot; }; struct Run { std::uintptr_t end; @@ -857,7 +854,7 @@ private: Ranges cleanRanges; Ranges freshRanges; std::map views; - std::map, Physical> physical; + std::map, std::shared_ptr> physical; std::map residentRuns; std::map splitAliases; std::size_t granularity = 0x10000; diff --git a/core/libs/prx/libkernel/DirectMemory/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp index ca31b70ce9..fb3ad0405c 100644 --- a/core/libs/prx/libkernel/DirectMemory/Jit.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -62,7 +62,10 @@ class JitObject { public: #ifdef _WIN32 JitObject(std::size_t bytes, HANDLE section) : bytes(bytes), section(section) {} - ~JitObject() { CloseHandle(section); } + ~JitObject() { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } #else explicit JitObject(std::size_t bytes) : bytes(bytes) {} #endif diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 08b6d4ea12..b44c48af61 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -172,6 +172,36 @@ void CheckPrivate() { VirtualFree(memory, 0, MEM_RELEASE); } +void CheckForgottenSectionKeepsNoPages() { + auto& mappings = Mappings::Get(); + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Bytes, nullptr); + auto* first = static_cast(mappings.Reserve(nullptr, Bytes)); + auto* second = static_cast(mappings.Reserve(nullptr, Bytes)); + Require(section && first && second, "cannot allocate forgotten section test memory"); + const auto resident = [&](unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "resident query failed"); + return info.State == MEM_COMMIT && info.Type == MEM_PRIVATE; + }; + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + mappings.Map(second, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(!resident(first) && !resident(second), "a second mapping kept the first resident"); + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Reset(second, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first), "a forgotten section's handle value kept its pages in the section"); + first[0] = 7; + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first) && first[0] == 0, "a forgotten section's handle value kept its unmapped bytes"); + mappings.Reset(first, Bytes); + VirtualFree(first, 0, MEM_RELEASE); + VirtualFree(second, 0, MEM_RELEASE); + CloseHandle(section); +} + void CheckConcurrentWrites() { Region region; std::atomic start{false}; @@ -199,6 +229,7 @@ int main() { CheckFailureAndHoles(); CheckPrivate(); CheckConcurrentWrites(); + CheckForgottenSectionKeepsNoPages(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 34cca1b9f5ba56e5cd54da89bd7f282f05cb0f74 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 21:26:11 +1300 Subject: [PATCH 326/353] test(avplayer): give the paused-jump restart test an event callback TestRestartAfterPausedJumpKeepsClockRunning created the player without events, so auto-start could start the source before the test enabled its streams by hand, and EnableStream then failed. With an event callback the player leaves starting to the test, as in TestUnsyncedVideoKeepsUpWithAudio. --- core/libs/tests/GuestAvPlayer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 4ae36f4396..0410bbab96 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1071,7 +1071,8 @@ void TestAutoStartKeepsStartedPlayback() { } void TestRestartAfterPausedJumpKeepsClockRunning() { - AvPlayerInitData init = InitData(nullptr); + Events events; + AvPlayerInitData init = InitData(&events); auto* player = sceAvPlayerInit(&init); Check(player != nullptr, "init failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); From 6b4d3c8ae6560300f1e652159c1e4368cca9528b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 21:40:28 +1300 Subject: [PATCH 327/353] Revert "fix(libSceVideoOut): export unknown Onimusha import NID" (#5608) The export turned YNw9Xk5iNWo from an unresolved import into a stub that throws. Dreaming Sarah (PPSA02929) calls it during start-up and now aborts with "YNw9Xk5iNWo not implemented" about 2 s in. Reverting until the function is implemented. --- core/libs/prx/libSceVideoOut/Unimplemented.cpp | 8 -------- docs/dev/debt/libSceVideoOut.md | 1 - 2 files changed, 9 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 24bf6dbc9d..ef58c55add 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,14 +7,6 @@ extern "C" { -APS5_EXPORT("YNw9Xk5iNWo", sceVideoOutUnknown_YNw9Xk5iNWo); -int APS5_VABI sceVideoOutUnknown_YNw9Xk5iNWo(int handle, void* status) { - (void)handle; - (void)status; - NotImplemented_nid_no_patch("YNw9Xk5iNWo"); - return 0; -} - int APS5_VABI sceVideoOutSetFlipMaster() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ac9a3fb89a..ce195aa306 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -8,7 +8,6 @@ ### Unknown function info -- [sceVideoOutUnknown_YNw9Xk5iNWo](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `YNw9Xk5iNWo`) - imported by Onimusha Way of the Sword (PPSA27836) on a newer SDK. The observed call shape is `(handle, status)` based on the import site, but the function's semantic name, output layout, and console behaviour are unknown; it is exposed only to let module loading reach the next unresolved dependency and reports an explicit unimplemented call when invoked - [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 - [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 From 2af88da01fa6eb429142e69c7dd56260239d2a7b Mon Sep 17 00:00:00 2001 From: TideOPL Date: Tue, 13 Oct 2026 07:06:54 +0100 Subject: [PATCH 328/353] perf(libc): walk the shared views of a write collect in order --- core/libs/prx/libc/include/WindowsMappings.hpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index c2c04713d5..04bf408fd0 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -330,6 +330,7 @@ public: } } } else if (clear && !collectable(address, end)) return false; + auto position = views.lower_bound(address & ~(pageBytes - 1)); for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -340,7 +341,7 @@ public: } } const auto base = cursor & ~(pageBytes - 1); - const auto found = views.find(base); + const auto found = viewAt(base, position); if (found != views.end() && found->second.resident) { if (!watchResident) return false; const auto available = capacity - *count; @@ -451,9 +452,10 @@ public: private: bool collectable(std::uintptr_t address, std::uintptr_t end) { + auto position = views.lower_bound(address & ~(pageBytes - 1)); for (auto cursor = address; cursor < end;) { const auto base = cursor & ~(pageBytes - 1); - if (const auto found = views.find(base); found != views.end()) { + if (const auto found = viewAt(base, position); found != views.end()) { if (found->second.protection == PAGE_NOACCESS || (found->second.resident && !watchResident)) return false; cursor = std::min(end, base + pageBytes); continue; @@ -854,6 +856,16 @@ private: Ranges cleanRanges; Ranges freshRanges; std::map views; + + using Views = std::map; + + Views::iterator viewAt(std::uintptr_t base, Views::iterator& position) { + if (position != views.end() && position->first < base) { + ++position; + if (position != views.end() && position->first < base) position = views.lower_bound(base); + } + return position != views.end() && position->first == base ? position : views.end(); + } std::map, std::shared_ptr> physical; std::map residentRuns; std::map splitAliases; From b14d8448558055b6ff36bc14cdbdc81fb2a2b1b0 Mon Sep 17 00:00:00 2001 From: TideOPL Date: Tue, 13 Oct 2026 07:26:48 +0100 Subject: [PATCH 329/353] perf(libc): remember ranges proven collectable until the mappings change --- core/libs/prx/libc/include/WindowsMappings.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 04bf408fd0..8df63459a7 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -35,6 +35,7 @@ public: std::vector> Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { std::vector> created; std::lock_guard lock(mutex); + collectableRanges.clear(); const auto end = reinterpret_cast(address) + bytes; for (auto cursor = reinterpret_cast(address); cursor < end;) { const auto memory = query(cursor); @@ -77,6 +78,7 @@ public: void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) { std::lock_guard lock(mutex); + collectableRanges.clear(); auto cursor = reinterpret_cast(address); reset(cursor, bytes); if (resident) watchResident = watched; @@ -145,6 +147,7 @@ public: void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { std::lock_guard lock(mutex); + collectableRanges.clear(); for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; it->second.armed = 0; @@ -452,6 +455,8 @@ public: private: bool collectable(std::uintptr_t address, std::uintptr_t end) { + const auto proven = collectableRanges.upper_bound(address); + if (proven != collectableRanges.begin() && std::prev(proven)->second >= end) return true; auto position = views.lower_bound(address & ~(pageBytes - 1)); for (auto cursor = address; cursor < end;) { const auto base = cursor & ~(pageBytes - 1); @@ -464,6 +469,7 @@ private: if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); } + remember(collectableRanges, address, end); return true; } @@ -775,6 +781,7 @@ private: } void reset(std::uintptr_t address, std::size_t bytes) { + collectableRanges.clear(); const auto end = address + bytes; forgetClean(address, end); breakRun(address); @@ -854,6 +861,7 @@ private: } Ranges cleanRanges; + Ranges collectableRanges; Ranges freshRanges; std::map views; From 759cc67baccbd675f066e9a04d1d5a254be82b0c Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 21:56:47 +1300 Subject: [PATCH 330/353] test(agc): validate the narrow constant store fixture through its word pointer Retained SPIR-V is a span now, and SpirvTools::Validate takes a vector or a pointer and a size. --- .../libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index ae99defb93..b63b6b04ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -44,7 +44,7 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { if (message != nullptr) diagnostics += std::string(message) + '\n'; }); - const bool valid = validator.Validate(words); + const bool valid = validator.Validate(words.data(), words.size()); Check(valid, std::string(name) + ": emitted invalid SPIR-V:\n" + diagnostics); #endif } From 206162549f17a226467181de09907272f001c74e Mon Sep 17 00:00:00 2001 From: Kikoarl <7354010+Kikoarl@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:54:08 +0200 Subject: [PATCH 331/353] perf(agc): eliminate DCC register clear fallback during color metadata pass --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp | 2 +- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +++--- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 +++++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6fcb807da9..477b211c16 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1006,8 +1006,10 @@ if(BUILD_TESTING) add_test(NAME agc_queued_write_groups COMMAND agc_driver_recorder_tests --queued-write-groups-only) add_test(NAME agc_slice_dirty COMMAND agc_driver_recorder_tests --slice-dirty-only) set_tests_properties(agc_slice_dirty PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) + set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 93f60adcdf..0d0d7b2d6f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -304,7 +304,7 @@ public: // captures, snapshots) takes the bytes through the flush hook, and this image alone is current. // False, naming why in `refusal`, when the fill must be stored as before. static constexpr std::uint32_t WholeImage = ~0u; - bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal); + bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear = false); // The cached, live image whose surface is exactly [address, address + bytes), if any (the newest // of several: see ClassifyFill). static std::shared_ptr FindLive(std::uint64_t address, std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index d3abd1ab1e..507e1c603a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -94,12 +94,12 @@ std::array clearTexel(const ColorTarget& color, DccKeys keys) { return texel; } -bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal) { +bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal, bool allowRegisterClear = false) { std::array repeated{}; for (std::size_t offset = 0; offset + elementBytes <= repeated.size(); offset += elementBytes) std::memcpy(repeated.data() + offset, texel.data(), elementBytes); std::array pattern{}; std::memcpy(pattern.data(), repeated.data(), repeated.size()); - return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal); + return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal, allowRegisterClear); } void writeTexels(const ColorTarget& color, const std::array& texel) { @@ -2582,7 +2582,7 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) const auto resident = metadataPassResident(context, color); if (resident != nullptr) { const char* refusal = nullptr; - const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; + const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal, true) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; if (current) { resident->MarkDirty(); MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d650643804..772036d992 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3813,7 +3813,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { return true; } -bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal) { +bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear) { // Debug aid (APS5_TRACE_FILL_COVER=1): the first layer covers refused, with the surface, the // pattern and the generation the refusal was memoized at (ClassifyFill's layerRefusedGeneration). static const bool trace = std::getenv("APS5_TRACE_FILL_COVER") != nullptr; @@ -3844,7 +3844,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u // Under a clear code reads see that value whatever the texels hold, and keys a recorded // kernel still writes are not in the bytes yet: only keys that read as uncompressed now let // the image stand for the texels. - if (Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { + if (!allowRegisterClear && Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { refusal = "keys pending"; return false; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 1807a56fc3..3415b5c76b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4866,7 +4866,9 @@ void metadataPassTests(const Device& device, Recorder& recorder) { Require(keys[0] == 0xff, "the recorded key fill landed before its batch ran"); Require(CurrentDccKeys(color.dccAddress, surfaceBytes) == DccKeys::ClearRegister, "the keys read past a pending fast-clear fill"); } + std::memset(texels, 0x77, surfaceBytes); RunColorMetadataPass(context, pass); + Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr && texels[0] == 0x77, "a fast clear recorded before the pass fell back to CPU write-back"); Require(keysUncompressed() && memoryHolds({0x10, 0x20, 0x40, 0x80}), "a fast clear recorded before the pass was not eliminated"); std::memset(texels, 0x66, surfaceBytes); RunColorMetadataPass(context, pass); @@ -6597,6 +6599,11 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--metadata-pass-only") { + metadataPassTests(device, recorder); + std::cout << "Color metadata pass tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { drawInputInPlaceTests(device, recorder); std::cout << "In-place draw input tests passed\n"; From b737308eedd87e601905fe923b52698a49520557 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Sun, 11 Oct 2026 20:39:41 +0200 Subject: [PATCH 332/353] fix(ssl): retain loaded certificate authorities --- .gitmodules | 3 + 3rdparty/mbedtls | 1 + core/libs/prx/libSceSsl/CMakeLists.txt | 70 +++- core/libs/prx/libSceSsl/Export.cpp | 468 +++++++++++++++++----- core/libs/prx/libSceSsl/MbedConfig.hpp | 4 + core/libs/prx/libSceSsl/SslTypes.hpp | 34 ++ core/libs/tests/GuestSslLoad.cpp | 176 ++++++++ core/libs/tests/SslCertificateFixture.hpp | 73 ++++ docs/dev/debt/libSceNet.md | 2 +- docs/dev/debt/libSceSsl.md | 4 +- 10 files changed, 732 insertions(+), 103 deletions(-) create mode 160000 3rdparty/mbedtls create mode 100644 core/libs/prx/libSceSsl/MbedConfig.hpp create mode 100644 core/libs/prx/libSceSsl/SslTypes.hpp create mode 100644 core/libs/tests/GuestSslLoad.cpp create mode 100644 core/libs/tests/SslCertificateFixture.hpp diff --git a/.gitmodules b/.gitmodules index ddbcc22956..efcc5aa70b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "3rdparty/libusb-cmake"] path = 3rdparty/libusb-cmake url = https://github.com/libusb/libusb-cmake.git +[submodule "3rdparty/mbedtls"] + path = 3rdparty/mbedtls + url = https://github.com/Mbed-TLS/mbedtls.git diff --git a/3rdparty/mbedtls b/3rdparty/mbedtls new file mode 160000 index 0000000000..068ff080b3 --- /dev/null +++ b/3rdparty/mbedtls @@ -0,0 +1 @@ +Subproject commit 068ff080b369adfac81509f9b57b2afabaf82dc5 diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index d66b9716ec..8142808e06 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -1,5 +1,68 @@ +function(configure_ssl_backend) + set(options ENABLE_PROGRAMS ENABLE_TESTING GEN_FILES USE_STATIC_MBEDTLS_LIBRARY + USE_SHARED_MBEDTLS_LIBRARY DISABLE_PACKAGE_CONFIG_AND_INSTALL MBEDTLS_CONFIG_FILE MBEDTLS_USER_CONFIG_FILE) + foreach(option IN LISTS options) + get_property(saved_${option}_exists CACHE ${option} PROPERTY TYPE SET) + if(saved_${option}_exists) + get_property(saved_${option}_type CACHE ${option} PROPERTY TYPE) + get_property(saved_${option}_help CACHE ${option} PROPERTY HELPSTRING) + get_property(saved_${option}_value CACHE ${option} PROPERTY VALUE) + endif() + endforeach() + set(ENABLE_PROGRAMS OFF) + set(ENABLE_TESTING OFF) + set(GEN_FILES OFF) + set(USE_STATIC_MBEDTLS_LIBRARY ON) + set(USE_SHARED_MBEDTLS_LIBRARY OFF) + set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON) + set(MBEDTLS_CONFIG_FILE "") + set(MBEDTLS_USER_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/MbedConfig.hpp") + foreach(option IN LISTS options) + if(option MATCHES "CONFIG_FILE$") + set(${option} "${${option}}" CACHE FILEPATH "" FORCE) + else() + set(${option} "${${option}}" CACHE BOOL "" FORCE) + endif() + endforeach() + if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM OR CMAKE_POLICY_VERSION_MINIMUM VERSION_LESS 3.5) + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + endif() + add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/mbedtls ${CMAKE_BINARY_DIR}/3rdparty/mbedtls EXCLUDE_FROM_ALL) + foreach(property LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) + get_target_property(libraries mbedcrypto ${property}) + if(libraries) + list(REMOVE_ITEM libraries everest) + set_property(TARGET mbedcrypto PROPERTY ${property} "${libraries}") + endif() + endforeach() + set_target_properties(everest PROPERTIES EXCLUDE_FROM_ALL ON) + foreach(option IN LISTS options) + if(saved_${option}_exists) + set(${option} "${saved_${option}_value}" CACHE ${saved_${option}_type} "${saved_${option}_help}" FORCE) + else() + unset(${option} CACHE) + endif() + endforeach() +endfunction() + +configure_ssl_backend() +set_target_properties(mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(MINGW) + target_compile_options(mbedcrypto PRIVATE -fno-asynchronous-unwind-tables) + target_compile_options(mbedx509 PRIVATE -fno-asynchronous-unwind-tables) + target_compile_options(p256m PRIVATE -fno-asynchronous-unwind-tables) +endif() + add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSsl PRIVATE mbedx509 mbedcrypto) +set_target_properties(libSceSsl PROPERTIES VISIBILITY_INLINES_HIDDEN ON) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceSsl PRIVATE LINKER:--exclude-libs,ALL) +endif() +if(WIN32) + target_link_libraries(libSceSsl PRIVATE bcrypt ws2_32) +endif() configure_windows_unwind(libSceSsl) if(BUILD_TESTING) @@ -8,8 +71,13 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_tests) add_test(NAME guest_ssl COMMAND guest_ssl_tests) + add_test_executable(guest_ssl_load_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslLoad.cpp) + target_include_directories(guest_ssl_load_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_load_tests PRIVATE libSceSsl libc) + configure_windows_unwind(guest_ssl_load_tests) + add_test(NAME guest_ssl_load COMMAND guest_ssl_load_tests) if(WIN32) - set_tests_properties(guest_ssl PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ssl guest_ssl_load PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index f0e94b445c..b12d6bb913 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,51 +1,160 @@ -#include -#include -#include -#include "SceTypes.hpp" +#include "SslTypes.hpp" #include "prx/libc/include/General.hpp" -#include -#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include #include #include #include - -// No network is emulated: contexts, templates and requests can be created, but any request -// that would touch the network fails with the library's network error. -static constexpr int ERROR_NETWORK = static_cast(0x80435001); -static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +#include +#include +#include namespace { -constexpr int ERROR_NOT_FOUND = static_cast(0x8095F004); -constexpr int ERROR_INVALID_ARG = static_cast(0x8095177A); -constexpr int ERROR_INVALID_ID = static_cast(0x8095F006); -constexpr int ERROR_INVALID_VALUE = static_cast(0x8095F007); +constexpr int ErrorNotFound = static_cast(0x8095f004); +constexpr int ErrorInvalidArg = static_cast(0x8095177a); +constexpr int ErrorInvalidId = static_cast(0x8095f006); -struct SslData { - char* ptr; - size_t size; +struct Pool { + std::size_t size, current = 0, peak = 0; + bool exhausted = false; }; -struct SslMemoryPoolStats { - size_t pool_size; - size_t max_inuse_size; - size_t current_inuse_size; - int32_t reserved; +struct alignas(std::max_align_t) Allocation { + Pool* pool; + std::size_t size; }; -struct SslCaCerts { - SslData* certs; - size_t num; - void* pool; +thread_local Pool* currentPool = nullptr; + +void* PoolCalloc(std::size_t count, std::size_t size) noexcept { + if (!currentPool) return nullptr; + if (size && count > std::numeric_limits::max() / size) { + currentPool->exhausted = true; + return nullptr; + } + const auto bytes = count * size; + if (bytes > currentPool->size - currentPool->current || bytes > std::numeric_limits::max() - sizeof(Allocation)) { + currentPool->exhausted = true; + return nullptr; + } + auto* allocation = static_cast(std::calloc(1, sizeof(Allocation) + bytes)); + if (!allocation) { + currentPool->exhausted = true; + return nullptr; + } + allocation->pool = currentPool; + allocation->size = bytes; + currentPool->current += bytes; + currentPool->peak = std::max(currentPool->peak, currentPool->current); + return allocation + 1; +} + +void PoolFree(void* pointer) noexcept { + if (!pointer) return; + auto* allocation = static_cast(pointer) - 1; + allocation->pool->current -= allocation->size; + mbedtls_platform_zeroize(pointer, allocation->size); + std::free(allocation); +} + +struct PoolScope { + Pool* previous; + explicit PoolScope(Pool& pool) : previous(std::exchange(currentPool, &pool)) { pool.exhausted = false; } + ~PoolScope() { currentPool = previous; } }; -struct SslCaList { - void** certs; - int num; +struct PoolDeleter { + void operator()(std::uint8_t* bytes) const { PoolFree(bytes); } +}; + +using Block = std::unique_ptr; + +Block Allocate(std::size_t size) { + Block block(static_cast(PoolCalloc(1, std::max(1, size)))); + if (!block) throw std::bad_alloc(); + return block; +} + +std::size_t Add(std::size_t left, std::size_t right) { + if (right > std::numeric_limits::max() - left) throw std::length_error("SSL allocation size overflow"); + return left + right; +} + +void CheckBackend(int error, const char* message) { + if (error == 0) return; + if (currentPool && currentPool->exhausted) throw std::bad_alloc(); + throw std::invalid_argument(message); +} + +struct CertificateChain; + +struct CertificateHandle { + std::weak_ptr owner; + const mbedtls_x509_crt* certificate; +}; + +struct CertificateChain { + mbedtls_x509_crt root; + std::vector handles; + CertificateChain() { mbedtls_x509_crt_init(&root); } + ~CertificateChain() { mbedtls_x509_crt_free(&root); } +}; + +struct PrivateKey { + mbedtls_pk_context key; + PrivateKey() { mbedtls_pk_init(&key); } + ~PrivateKey() { mbedtls_pk_free(&key); } +}; + +struct Random { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + Random() { + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + } + ~Random() { + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + } + void Seed() { + CheckBackend(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0), "SSL random generator initialization failed"); + } +}; + +struct Identity { + std::shared_ptr chain; + PrivateKey key; +}; + +struct Snapshot { + Block data; + std::vector> certificates; + std::size_t count; +}; + +struct Context { + Pool pool; + std::vector> authorities; + std::shared_ptr identity; + std::vector> certificateHandles; + std::unordered_map caLists; + std::unordered_map caCerts; + explicit Context(std::size_t size) : pool{size} {} }; -static_assert(sizeof(SslCaList) == 16); using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); @@ -57,85 +166,268 @@ struct SslConnection { void* verifyArgument; }; -std::map g_connections; +struct Runtime { + std::mutex mutex; + std::unordered_map> contexts; + std::unordered_map connections; + unsigned nextHandle = 1; + Runtime() { + if (mbedtls_platform_set_calloc_free(PoolCalloc, PoolFree) != 0) + throw std::runtime_error("SSL allocator initialization failed"); + } +}; -void RequireContext(const char* function, int sslCtxId) { - std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(sslCtxId)) throw std::invalid_argument(std::string(function) + ": unknown context"); +Runtime& State() { + static auto* runtime = new Runtime; + return *runtime; +} + +Context& GetContext(int handle) { + const auto found = State().contexts.find(handle); + if (found == State().contexts.end()) throw std::invalid_argument("Invalid SSL context"); + return *found->second; } SslConnection& RequireConnection(const char* function, int sslConnectionId) { - const auto connection = g_connections.find(sslConnectionId); - if (connection == g_connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); - if (!g_pools.contains(connection->second.context)) { + const auto connection = State().connections.find(sslConnectionId); + if (connection == State().connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); + if (!State().contexts.contains(connection->second.context)) { throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); } return connection->second; } +struct Input { + Block bytes; + std::size_t size; +}; + +Input CopyInput(const SceSsl::Data& data) { + if (!data.ptr || !data.size) throw std::invalid_argument("Empty SSL certificate/key data"); + auto bytes = Allocate(Add(data.size, 1)); + std::memcpy(bytes.get(), data.ptr, data.size); + auto size = data.size; + std::size_t first = 0; + while (first < size && (bytes.get()[first] == ' ' || bytes.get()[first] == '\r' || bytes.get()[first] == '\n' || bytes.get()[first] == '\t')) ++first; + const bool pem = first < size && bytes.get()[first] == '-'; + if (pem) { + if (bytes.get()[size - 1] != 0) ++size; + if (std::memchr(bytes.get(), 0, size - 1)) throw std::invalid_argument("Embedded null in SSL PEM data"); + } else { + auto* position = bytes.get(); + std::size_t length = 0; + CheckBackend(mbedtls_asn1_get_tag(&position, bytes.get() + size, &length, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE), + "Invalid SSL DER sequence"); + if (length != static_cast(bytes.get() + size - position)) throw std::invalid_argument("Trailing SSL DER data"); + } + return {std::move(bytes), size}; +} + +std::shared_ptr ParseCertificate(const SceSsl::Data& data) { + auto chain = std::make_shared(); + const auto input = CopyInput(data); + CheckBackend(mbedtls_x509_crt_parse(&chain->root, input.bytes.get(), input.size), "Invalid SSL certificate chain"); + return chain; +} + +std::size_t CertificateCount(const Context& context) { + std::size_t count = 0; + for (const auto& chain : context.authorities) count = Add(count, chain->handles.size()); + return count; +} } extern "C" { -int APS5_VABI sceSslFreeCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); + const auto handle = static_cast(state.nextHandle); + state.contexts.emplace(handle, std::make_unique(poolSize)); + ++state.nextHandle; + return handle; +} + +int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.contexts.erase(sslCtxId); return 0; } -int APS5_VABI sceSslGetCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; - return ERROR_NOT_FOUND; -} - -int APS5_VABI sceSslGetCaList(int sslCtxId, SslCaList* caList) { - if (!caList) return ERROR_INVALID_ARG; - RequireContext(__func__, sslCtxId); - *caList = {}; - return ERROR_NOT_FOUND; -} - -int APS5_VABI sceSslFreeCaList(int sslCtxId, SslCaList* caList) { - if (!caList) return ERROR_INVALID_ARG; - RequireContext(__func__, sslCtxId); - if (caList->certs || caList->num) throw std::invalid_argument("sceSslFreeCaList: list not returned by sceSslGetCaList"); +int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { + if (!stats) return static_cast(0x8095F007); + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + std::memset(stats, 0, sizeof(*stats)); + return static_cast(0x8095F006); + } + const auto& pool = found->second->pool; + *stats = {pool.size, pool.peak, pool.current, 0}; return 0; } -int APS5_VABI sceSslInit_nid_postfix(uint64_t pool_size) { - const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; - return id; +int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, SceSsl::Data* cert, SceSsl::Data* privateKey) { + if (caCertNum < 0 || (caCertNum && !caList) || (!!cert != !!privateKey)) + throw std::invalid_argument("Invalid SSL certificate load arguments"); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + std::vector> authorities; + std::vector> handles; + for (int index = 0; index < caCertNum; ++index) { + if (!caList[index]) throw std::invalid_argument("Missing SSL certificate descriptor"); + auto chain = ParseCertificate(*caList[index]); + for (auto* entry = &chain->root; entry; entry = entry->next) { + auto handle = std::make_unique(CertificateHandle{chain, entry}); + chain->handles.push_back(handle.get()); + handles.push_back(std::move(handle)); + } + authorities.push_back(std::move(chain)); + } + std::shared_ptr identity; + if (cert) { + identity = std::make_shared(); + identity->chain = ParseCertificate(*cert); + const auto key = CopyInput(*privateKey); + Random random; + random.Seed(); + CheckBackend(mbedtls_pk_parse_key(&identity->key.key, key.bytes.get(), key.size, nullptr, 0, + mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); + CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &identity->key.key, mbedtls_ctr_drbg_random, &random.drbg), + "SSL client certificate and private key do not match"); + } + context.authorities.reserve(Add(context.authorities.size(), authorities.size())); + context.certificateHandles.reserve(Add(context.certificateHandles.size(), handles.size())); + for (auto& chain : authorities) context.authorities.push_back(std::move(chain)); + for (auto& handle : handles) context.certificateHandles.push_back(std::move(handle)); + if (identity) context.identity = std::move(identity); + return 0; } -int APS5_VABI sceSslTerm_nid_postfix(int ssl_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(ssl_ctx_id); +int APS5_VABI sceSslUnloadCert(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + context.authorities.clear(); + context.identity.reset(); + return 0; +} + +int APS5_VABI sceSslGetCaList(int sslCtxId, SceSsl::CaList* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > static_cast(std::numeric_limits::max())) throw std::length_error("Too many SSL authorities"); + Snapshot snapshot{Allocate(count * sizeof(void*)), context.authorities, count}; + auto** pointers = reinterpret_cast(snapshot.data.get()); + std::size_t index = 0; + for (const auto& chain : snapshot.certificates) for (auto* handle : chain->handles) pointers[index++] = handle; + context.caLists.emplace(pointers, std::move(snapshot)); + *output = {pointers, static_cast(count)}; + return 0; +} + +int APS5_VABI sceSslFreeCaList(int sslCtxId, SceSsl::CaList* list) { + if (!list) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + if (!list->certs && list->num == 0) return 0; + const auto found = context.caLists.find(list->certs); + if (found == context.caLists.end() || list->num < 0 || static_cast(list->num) != found->second.count) + throw std::invalid_argument("Invalid SSL CA list allocation"); + context.caLists.erase(found); + *list = {}; + return 0; +} + +int APS5_VABI sceSslGetCaCerts(int sslCtxId, SceSsl::CaCerts* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + *output = {}; + return ErrorNotFound; + } + auto& context = *found->second; + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > std::numeric_limits::max() / sizeof(SceSsl::Data)) throw std::length_error("Too many SSL authorities"); + auto total = count * sizeof(SceSsl::Data); + for (const auto& chain : context.authorities) + for (auto* entry = &chain->root; entry; entry = entry->next) total = Add(total, entry->raw.len); + Snapshot snapshot{Allocate(total), {}, count}; + auto* data = reinterpret_cast(snapshot.data.get()); + auto* next = snapshot.data.get() + count * sizeof(SceSsl::Data); + std::size_t index = 0; + for (const auto& chain : context.authorities) { + for (auto* entry = &chain->root; entry; entry = entry->next) { + data[index++] = {next, entry->raw.len}; + std::memcpy(next, entry->raw.p, entry->raw.len); + next += entry->raw.len; + } + } + context.caCerts.emplace(data, std::move(snapshot)); + *output = {data, count, data}; + return 0; +} + +int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { + if (!certificates) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found != state.contexts.end()) { + auto& snapshots = found->second->caCerts; + const auto allocation = snapshots.find(certificates->pool); + if (allocation != snapshots.end()) { + if (certificates->certs != certificates->pool || certificates->num != allocation->second.count) + throw std::invalid_argument("Invalid SSL CA certificate allocation"); + snapshots.erase(allocation); + } + } + *certificates = {}; return 0; } int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); - std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(sslCtxId)) return ERROR_INVALID_ID; - const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_connections[id] = {sslCtxId, sockId, host, nullptr, nullptr}; + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + auto& state = State(); + if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL connection handles exhausted"); + const auto id = static_cast(state.nextHandle); + state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr}); + ++state.nextHandle; return id; } int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { - std::lock_guard lock(g_poolsMutex); - if (!g_connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + std::lock_guard lock(State().mutex); + if (!State().connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); return 0; } int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { - std::lock_guard lock(g_poolsMutex); + std::lock_guard lock(State().mutex); SslConnection& connection = RequireConnection(__func__, sslConnectionId); connection.verifyCallback = cbfunc; connection.verifyArgument = userArg; @@ -176,28 +468,6 @@ int APS5_VABI sceSslGetSerialNumber() { return 0; } -int APS5_VABI sceSslLoadCert() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslUnloadCert(int sslCtxId) { - RequireContext(__func__, sslCtxId); - return 0; -} - -int APS5_VABI sceSslGetMemoryPoolStats(int ssl_ctx_id, SslMemoryPoolStats* stats) { - if (stats == nullptr) return ERROR_INVALID_VALUE; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(ssl_ctx_id); - if (pool == g_pools.end()) { - std::memset(stats, 0, sizeof(*stats)); - return ERROR_INVALID_ID; - } - *stats = {static_cast(pool->second), 0, 0, 0}; - return 0; -} - int APS5_VABI sceSslFreeSslCertName(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp new file mode 100644 index 0000000000..ab4361c8a7 --- /dev/null +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define MBEDTLS_PLATFORM_MEMORY +#undef MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED diff --git a/core/libs/prx/libSceSsl/SslTypes.hpp b/core/libs/prx/libSceSsl/SslTypes.hpp new file mode 100644 index 0000000000..18fc70f740 --- /dev/null +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace SceSsl { + +struct Data { + std::uint8_t* ptr; + std::size_t size; +}; + +struct CaCerts { + Data* certs; + std::size_t num; + void* pool; +}; + +struct CaList { + void** certs; + int num; +}; + +struct MemoryPoolStats { + std::size_t poolSize, maxInuseSize, currentInuseSize; + std::uint32_t reserved; +}; + +static_assert(sizeof(Data) == 0x10); +static_assert(sizeof(CaCerts) == 0x18); +static_assert(sizeof(CaList) == 0x10); +static_assert(sizeof(MemoryPoolStats) == 0x20); + +} diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp new file mode 100644 index 0000000000..aaa93f2962 --- /dev/null +++ b/core/libs/tests/GuestSslLoad.cpp @@ -0,0 +1,176 @@ +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, SceSsl::MemoryPoolStats*); +int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslUnloadCert(int); +int APS5_VABI sceSslGetCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslFreeCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslGetCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +} + +namespace { + +constexpr int NotFound = static_cast(0x8095F004); + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void Throws(TAction action) { + bool caught = false; + try { action(); } catch (const std::exception&) { caught = true; } + Check(caught, "unsupported or malformed load was accepted"); +} + +SceSsl::Data Data(const char* text) { + return {reinterpret_cast(const_cast(text)), std::strlen(text)}; +} + +SceSsl::MemoryPoolStats Stats(int id) { + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(id, &stats) == 0, "stats failed"); + Check(stats.currentInuseSize <= stats.maxInuseSize && stats.maxInuseSize <= stats.poolSize && !stats.reserved, + "pool accounting bounds failed"); + return stats; +} + +void CheckRoot(const SceSsl::CaCerts& snapshot) { + Check(snapshot.num == 1 && snapshot.certs[0].size == RootDer.size(), "CA count or DER size differs"); + Check(std::memcmp(snapshot.certs[0].ptr, RootDer.data(), RootDer.size()) == 0, "CA bytes changed"); +} + +void Lifecycle() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + std::string original = RootPem; + auto data = Data(original.c_str()); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "initial CA load failed"); + original.assign(original.size(), '?'); + Check(Stats(id).currentInuseSize > RootDer.size(), "loaded certificate is not retained/accounted"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "encoded CA query failed"); + CheckRoot(bytes); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::CaList empty{}; + Check(sceSslGetCaList(other, &empty) == NotFound, "CA escaped its context"); + auto foreign = handles; + Throws([&] { sceSslFreeCaList(other, &foreign); }); + Check(sceSslUnloadCert(id) == 0, "unload failed"); + Check(sceSslGetCaList(id, &empty) == NotFound && !empty.certs && !empty.num, "unload retained active CA"); + CheckRoot(bytes); + Check(sceSslFreeCaList(id, &handles) == 0 && !handles.certs && !handles.num, "handle snapshot release failed"); + Check(sceSslFreeCaCerts(id, &bytes) == 0 && !bytes.certs && !bytes.pool && !bytes.num, "encoded snapshot release failed"); + Check(Stats(id).currentInuseSize == 0, "certificate allocation leaked after release"); + Throws([&] { sceSslFreeCaList(id, &foreign); }); + Check(sceSslTerm_nid_postfix(id) == 0 && sceSslTerm_nid_postfix(other) == 0, "term failed"); +} + +void ConnectionLifetime() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + const int connection = sceSslCreateConnection(id, -1, "example.invalid"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + Check(connection > 0 && connection != id && connection != other, "SSL handles overlap"); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load with a connection failed"); + Check(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0, "certificate load invalidated the connection"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA query with a connection failed"); + CheckRoot(bytes); + SceSsl::CaCerts empty{}; + Check(sceSslGetCaCerts(other, &empty) == NotFound, "CA escaped into another context"); + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(connection, &stats) == static_cast(0x8095F006), "connection accepted as context"); + const int retained = sceSslCreateConnection(id, -1, "example.invalid"); + Check(retained > 0 && retained != connection && retained != other, "SSL connection handle was reused"); + Check(sceSslDeleteConnection(connection) == 0, "connection deletion failed"); + Check(sceSslUnloadCert(id) == 0, "credential unload failed with a retained connection"); + CheckRoot(bytes); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); + Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); + Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); + Throws([&] { sceSslSetVerifyCallback(retained, nullptr, nullptr); }); + Check(sceSslDeleteConnection(retained) == 0, "terminated context's connection cannot be released"); + Check(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); + Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); +} + +void AtomicFailures() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::Data der{const_cast(RootDer.data()), RootDer.size()}; + auto* pointer = &der; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "DER CA load failed"); + const auto before = Stats(id).currentInuseSize; + auto bad = Data("not a certificate"); + SceSsl::Data* mixed[]{&der, &bad}; + Throws([&] { sceSslLoadCert(id, 2, mixed, nullptr, nullptr); }); + Check(Stats(id).currentInuseSize == before, "failed chain changed live allocation"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA vanished after failure"); + CheckRoot(bytes); + Check(sceSslFreeCaCerts(id, &bytes) == 0, "snapshot free failed"); + Throws([&] { sceSslLoadCert(id, -1, nullptr, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 1, nullptr, nullptr, nullptr); }); + SceSsl::Data* absent = nullptr; + Throws([&] { sceSslLoadCert(id, 1, &absent, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &der, nullptr); }); + Check(Stats(id).currentInuseSize == before, "argument failure changed retained CA"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Throws([&] { sceSslLoadCert(id, 1, &pointer, nullptr, nullptr); }); + const int tiny = sceSslInit_nid_postfix(32); + Throws([&] { sceSslLoadCert(tiny, 1, &pointer, nullptr, nullptr); }); + Check(Stats(tiny).currentInuseSize == 0, "failed small-pool load leaked"); + Check(sceSslTerm_nid_postfix(tiny) == 0, "small-pool term failed"); +} + +void ClientIdentity() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto client = Data(ClientPem); + auto key = Data(ClientKey); + auto wrong = Data(RootKey); + Check(sceSslLoadCert(id, 0, nullptr, &client, &key) == 0, "client identity load failed"); + const auto before = Stats(id).currentInuseSize; + Check(before > 0, "identity not retained"); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &client, &wrong); }); + Check(Stats(id).currentInuseSize == before, "mismatched key changed identity"); + Check(sceSslUnloadCert(id) == 0 && Stats(id).currentInuseSize == 0, "identity release failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); +} + +} + +int main() { + try { + Lifecycle(); + AtomicFailures(); + ClientIdentity(); + ConnectionLifetime(); + std::vector workers; + for (int i = 0; i < 3; ++i) workers.emplace_back([] { for (int n = 0; n < 4; ++n) Lifecycle(); }); + for (auto& worker : workers) worker.join(); + std::puts("SSL load lifecycle passed"); + } catch (const std::exception& error) { + std::fprintf(stderr, "SSL load lifecycle failed: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp new file mode 100644 index 0000000000..82d0ad962b --- /dev/null +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -0,0 +1,73 @@ +#pragma once +#include +#include +inline constexpr char RootPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIByDCCAW6gAwIBAgIDAIChMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBCMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMRwwGgYDVQQDDBNBbnlQUzUgZml4dHVy +ZSByb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGBewuI37COE1s3xt1y52 +kxoDW1vLl0IzSIlIjKyLrj80kjZWqXOR6X4Th8cyBPVeQkHh0CE+TjZtKpI3CL/H +6aNTMFEwHQYDVR0OBBYEFFZolbTcGqYVMtT6BD37g9yqaAzcMB8GA1UdIwQYMBaA +FFZolbTcGqYVMtT6BD37g9yqaAzcMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E +AwIDSAAwRQIgUQ4NhPYRVB5YgwemEZ5vm76nEY97hJYlq/GymdmmsLgCIQDKMTQC +eNbT9zlbdI+uo50utM9rDZ8kSBx5RsuAVj8M3w== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ClientPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIB3TCCAYOgAwIBAgIDEjRWMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBoMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMSIwDgYDVQQLDAdQcmltYXJ5MBAGA1UE +CwwJU2Vjb25kYXJ5MR4wHAYDVQQDDBVBbnlQUzUgZml4dHVyZSBjbGllbnQwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNEDnWt +ZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsXo0IwQDAdBgNV +HQ4EFgQUwVSZJzELeQ6acITiH929Z4kUuEUwHwYDVR0jBBgwFoAUVmiVtNwaphUy +1PoEPfuD3KpoDNwwCgYIKoZIzj0EAwIDSAAwRQIhAIJ3dzC+ddOvUbBXg4sCYTG3 +xXS9+jF9gxro9MqK1ZenAiA+l/Y8EMrvzjXx7kN5ZV+cVSnH1Ngn3kqaE8w7gIRK +NQ== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char RootKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHIXi+x3Z7ozqaKSg +w4mBVS67OC+pzhTpjT6EhQ9/V32hRANCAAQYF7C4jfsI4TWzfG3XLnaTGgNbW8uX +QjNIiUiMrIuuPzSSNlapc5HpfhOHxzIE9V5CQeHQIT5ONm0qkjcIv8fp +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char ClientKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHoANtFzCHilXfZPd +GmO1G+NKG5sSUnUWmDrU29+IaQahRANCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNE +DnWtZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsX +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr std::array RootDer{ + 0x30, 0x82, 0x01, 0xc8, 0x30, 0x82, 0x01, 0x6e, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x03, 0x00, + 0x80, 0xa1, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x42, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x15, 0x30, + 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x54, + 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x13, 0x41, + 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x20, 0x72, 0x6f, + 0x6f, 0x74, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x36, 0x31, 0x30, 0x30, 0x38, 0x32, 0x32, 0x31, 0x34, + 0x30, 0x32, 0x5a, 0x17, 0x0d, 0x33, 0x36, 0x31, 0x30, 0x30, 0x35, 0x32, 0x32, 0x31, 0x34, 0x30, + 0x32, 0x5a, 0x30, 0x42, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, + 0x42, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, + 0x53, 0x35, 0x20, 0x54, 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x13, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, + 0x65, 0x20, 0x72, 0x6f, 0x6f, 0x74, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + 0x04, 0x18, 0x17, 0xb0, 0xb8, 0x8d, 0xfb, 0x08, 0xe1, 0x35, 0xb3, 0x7c, 0x6d, 0xd7, 0x2e, 0x76, + 0x93, 0x1a, 0x03, 0x5b, 0x5b, 0xcb, 0x97, 0x42, 0x33, 0x48, 0x89, 0x48, 0x8c, 0xac, 0x8b, 0xae, + 0x3f, 0x34, 0x92, 0x36, 0x56, 0xa9, 0x73, 0x91, 0xe9, 0x7e, 0x13, 0x87, 0xc7, 0x32, 0x04, 0xf5, + 0x5e, 0x42, 0x41, 0xe1, 0xd0, 0x21, 0x3e, 0x4e, 0x36, 0x6d, 0x2a, 0x92, 0x37, 0x08, 0xbf, 0xc7, + 0xe9, 0xa3, 0x53, 0x30, 0x51, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, + 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, 0xdc, + 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, + 0x14, 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, + 0xdc, 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, + 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, 0x51, 0x0e, 0x0d, 0x84, 0xf6, 0x11, 0x54, + 0x1e, 0x58, 0x83, 0x07, 0xa6, 0x11, 0x9e, 0x6f, 0x9b, 0xbe, 0xa7, 0x11, 0x8f, 0x7b, 0x84, 0x96, + 0x25, 0xab, 0xf1, 0xb2, 0x99, 0xd9, 0xa6, 0xb0, 0xb8, 0x02, 0x21, 0x00, 0xca, 0x31, 0x34, 0x02, + 0x78, 0xd6, 0xd3, 0xf7, 0x39, 0x5b, 0x74, 0x8f, 0xae, 0xa3, 0x9d, 0x2e, 0xb4, 0xcf, 0x6b, 0x0d, + 0x9f, 0x24, 0x48, 0x1c, 0x79, 0x46, 0xcb, 0x80, 0x56, 0x3f, 0x0c, 0xdf, +}; diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323dc..ea8531c106 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -3,7 +3,7 @@ ### Unknown function info - PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. -- [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) +- [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters ### Functional diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index a789cb5729..f5b9ed5354 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -5,8 +5,8 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetCaList, sceSslFreeCaList, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and the 16-byte `SceSslCaList` layout (an array of certificate handles and a 32-bit count) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs) and shadPS4's `OrbisSslCaList`, not confirmed for the PS5. On the console the list holds the certificate authorities the SSL service has; [shadPS4](https://github.com/shadps4-emu/shadPS4/pull/4104) returns a placeholder certificate from `sceSslGetCaCerts` because Rise of the Tomb Raider fails its initialization checks without one. No root certificate bundle is shipped and no loaded certificate is kept, so here a context holds none: `sceSslGetCaList` clears the list and returns `NOT_FOUND`, as `sceSslGetCaCerts` does, `sceSslFreeCaList` accepts only that cleared list, and `sceSslUnloadCert` (imported by PPSA15508) has nothing to drop and is assumed to return 0. A null list returns `0x8095177A`, which `sceSslGetCaCerts` and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/libs/network.cpp) return for a null argument (shadPS4, PS4, returns `0x8095F007`). An unknown context throws (KytyPS5 returns `SSL_ERROR_INVALID_ID`, `0x8095F006`, from `sceSslGetCaCerts`), and so does a list `sceSslGetCaList` did not return -- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). They act on a connection, which `sceSslCreateConnection` does not create yet, so they throw until connections exist +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer +- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). Connection storage exists, but their signatures and option behavior remain unknown, so they still throw ### Functional From 6f830952fbd3494468bb89068b7047a0fe50a674 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 22:37:06 +1300 Subject: [PATCH 333/353] ci(release): check out nested submodules build.yml runs git submodule update --init --recursive, but release.yml checked out only the top-level submodules. A submodule with its own submodules, such as Mbed TLS's framework in #3897, would leave the release configure step without them. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f074818c91..1cc1d9aa9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: true + submodules: recursive - id: toolchain if: matrix.platform == 'windows' uses: actions/cache@v6 From 13cc3a9ea685a23b77b9bb76a5f4248af42d6fc3 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 22:37:42 +1300 Subject: [PATCH 334/353] fix(shader): size per-invocation LDS by the append counter's offset FunctionLdsDwords bounds every LDS access by its address argument and gives up on opcodes without a dword count. DataAppend and DataConsume have neither: their first argument is M0, which only places GDS counters, and the emitter addresses the LDS counter by the instruction offset alone. Any pixel or vertex-path program with ds_append or ds_consume therefore declared the full 8192-dword Function array per invocation. LLVM 20 llvmpipe needs about 6 s to compile that array in the new pixel LDS read-modify-write test, which ran past the 5 s graphics fence on CI. The counter now takes the dword at its offset. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 7 +++++++ core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2554a475bc..52ff191b46 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3142,6 +3142,13 @@ void verifyFunctionLdsBound() { }); require(unbounded == FunctionLdsDwordLimit && unboundedRequirements.functionLdsDwords == FunctionLdsDwordLimit, "function LDS: an address without a bound must keep the full array"); + const auto [counter, counterRequirements] = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { + auto& m0 = emit(IrOpcode::GetUserData, IrType::U32, {&constant(0u)}); + emit(IrOpcode::DataAppend, IrType::U32, {&m0, &active}, 2u); + emit(IrOpcode::DataConsume, IrType::U32, {&m0, &active}, 1u); + }); + require(counter == 192u && counterRequirements.functionLdsDwords == 192u, "function LDS: an LDS append or consume must size the array by its offset, not by M0"); + const auto wrapping = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { emit(IrOpcode::LoadSharedU32, IrType::U32, {&constant(0x20u), &active}, 3u); }).first; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 4742b649a6..dd83663928 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -224,11 +224,17 @@ std::uint32_t FunctionLdsDwords(const IrProgram& program) { std::uint64_t needed = 0; for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { - if (SharedAccessOf(inst->Opcode()) == SharedAccess::None) continue; + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None) continue; const auto index = inst->Flags().index; if (index >= program.Resources().memoryInfo.size()) return FunctionLdsDwordLimit; const auto& memory = program.Resources().memoryInfo[index]; if (memory.kind != ResourceKind::Lds) continue; + if (access == SharedAccess::Append || access == SharedAccess::Consume) { + needed = std::max(needed, (std::uint64_t{memory.offset} >> 2u) + 1u); + if (needed >= FunctionLdsDwordLimit) return FunctionLdsDwordLimit; + continue; + } const auto dwords = SharedAccessDwords(inst->Opcode()); if (dwords == 0u || inst->ArgumentCount() == 0) return FunctionLdsDwordLimit; const auto address = bound.Of(inst->Argument(0)); From 3b8bf7eab5b2351988501f69fcaf5f1d88d6d18b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 22:37:42 +1300 Subject: [PATCH 335/353] test(agc): keep the pixel LDS read-modify-write slots within 1 KiB Each pixel used its own 16-byte slot across 512 pixels, so the per-invocation LDS array held 8 KiB for a test that only needs one slot per lane of a wave. 64 slots and the counter right after them keep the array at 320 dwords, which software rasterizers compile in a fraction of the time. --- .../tests/execution/PixelLdsReadModifyWrite.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp index 17a71bd6ff..cf2496bda8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp @@ -21,7 +21,8 @@ constexpr std::uint32_t Height = 8; constexpr std::uint32_t Results = 8; constexpr std::uint32_t Factor = 0x00a5c3e1u; constexpr std::uint32_t Counter = 0x1234u; -constexpr std::uint32_t CounterOffset = 0x2000u; +constexpr std::uint32_t Slots = 64; +constexpr std::uint32_t CounterOffset = Slots * 16u; alignas(256) std::array Pixels{}; alignas(256) std::array Outputs{}; @@ -39,7 +40,7 @@ std::vector PixelCode() { code.push_back(address | (data0 << 8u) | (data1 << 16u) | (destination << 24u)); }; vop2(0x36000000, 8, 0xff, 6); - code.push_back(Width * Height - 1u); + code.push_back(Slots - 1u); vop2(0x34000000, 8, 0x84, 8); code.push_back(0x7e126f06); vop2(0x16000000, 10, 0xff, 6); From aa2abf801bc4eaaba5ce6816b81c8f5a17157bcb Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Tue, 13 Oct 2026 13:32:36 +0400 Subject: [PATCH 336/353] fix(shader): ignore unused descriptor fields for scalar buffer loads --- .../execution/BufferDescriptorRuntime.cpp | 36 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 4 +++ docs/dev/debt/shader.md | 1 + 3 files changed, 41 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 45364ef783..5e4bd47bfc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -93,6 +93,41 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { } } +void ScalarReads(AgcDriver::VulkanDevice& device) { + constexpr std::array scalarCode{0x34020082u, 0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x7e040208u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + std::array userData{}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + auto request = Request(userData, device.ComputeTarget(Lanes), {}); + request.shader.code = scalarCode; + request.shader.codeAddress = reinterpret_cast(scalarCode.data()); + request.useCache = false; + std::uint32_t expected = 0; + std::memcpy(&expected, Input.data() + 4u, sizeof(expected)); + for (const auto word : {0u, 0x31016facu, 0x91b00f2eu, 0xffffffffu}) { + auto input = BufferDescriptor(Input.data(), InputBytes); + input[3] = word; + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }), "scalar buffer read changed with ignored descriptor word " + Hex(word)); + } + constexpr std::array mixedCode{0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0x7e060208u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, "vector buffer descriptor failed for another reason"); + continue; + } + throw std::runtime_error("a vector or mixed buffer access accepted a non-buffer descriptor type"); + } +} + } int main() { @@ -102,6 +137,7 @@ int main() { for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); Run(*device, false); Run(*device, true); + ScalarReads(*device); std::puts("buffer descriptor runtime tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ce23f6fb6c..a5e993ea3e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -586,6 +586,10 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, const std::vector& tables) { for (std::uint32_t i = 0u; i < plan.info.buffers.size(); ++i) { + const auto& buffer = plan.info.buffers[i]; + const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias + && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); + if (scalarOnly) snapshot.buffers.at(i).dwords[3] = 0u; const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1aca9b11e9..050c4613c6 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -117,3 +117,4 @@ - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests +- [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. From bfab566fef533f62b76b278fac831a1eb5c877df Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Wed, 14 Oct 2026 10:32:28 +0400 Subject: [PATCH 337/353] fix(shader): normalize reserved buffer bits before scalar binding --- .../execution/BufferDescriptorRuntime.cpp | 36 ++++++++++++++----- .../Optimization/src/ResourceMaterializer.cpp | 5 ++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 5e4bd47bfc..bf04d4c6f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -104,15 +104,18 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { request.useCache = false; std::uint32_t expected = 0; std::memcpy(&expected, Input.data() + 4u, sizeof(expected)); - for (const auto word : {0u, 0x31016facu, 0x91b00f2eu, 0xffffffffu}) { - auto input = BufferDescriptor(Input.data(), InputBytes); - input[3] = word; - std::copy(input.begin(), input.end(), userData.begin()); - Output.fill(Fill); - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1); - device.WaitIdle(); - Require(std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }), "scalar buffer read changed with ignored descriptor word " + Hex(word)); + for (const auto ignoredWord1 : {0u, 0x40000000u}) { + for (const auto word : {0u, 0x31016facu, 0x91b00f2eu, 0xffffffffu}) { + auto input = BufferDescriptor(Input.data(), InputBytes); + input[1] |= ignoredWord1; + input[3] = word; + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }), "scalar buffer read changed with ignored descriptor word " + Hex(word)); + } } constexpr std::array mixedCode{0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0x7e060208u, 0xe0701000u, 0x80010301u, 0xbf810000u}; for (const auto code : {std::span(Code), std::span(mixedCode)}) { @@ -126,6 +129,21 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { } throw std::runtime_error("a vector or mixed buffer access accepted a non-buffer descriptor type"); } + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + auto input = BufferDescriptor(Input.data(), InputBytes); + input[1] |= 0x40000000u; + std::copy(input.begin(), input.end(), userData.begin()); + const auto result = ShaderRecompiler::Recompile(request); + try { + device.Dispatch(result, 1, 1, 1); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor has reserved bits set") != std::string::npos, "vector or mixed buffer descriptor failed for another reason"); + continue; + } + throw std::runtime_error("a vector or mixed buffer access accepted reserved descriptor bits"); + } } } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index a5e993ea3e..4302dac126 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -589,7 +589,10 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, c const auto& buffer = plan.info.buffers[i]; const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); - if (scalarOnly) snapshot.buffers.at(i).dwords[3] = 0u; + if (scalarOnly) { + snapshot.buffers.at(i).dwords[1] &= ~0x40000000u; + snapshot.buffers.at(i).dwords[3] = 0u; + } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); From c8b3a37533819f8534e779a42a5871f255b13c3e Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 11:15:40 +0300 Subject: [PATCH 338/353] feat(agc): emulate default eight-sample dithered alpha coverage --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 18 + .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 52 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 93 ++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 320 ++++++++++++++- .../tests/execution/AlphaToCoverage.cpp | 131 +++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- .../Optimization/ShaderStageInputInfo.hpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 2 +- core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 43 ++ .../src/ShaderInputInfoBuilder.cpp | 2 + .../tests/RequestSerializerTests.cpp | 387 +++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 18 files changed, 1019 insertions(+), 45 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6fcb807da9..a3699750ff 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1227,6 +1227,24 @@ target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +if(BUILD_TESTING) + add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) + set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8) + set_tests_properties(agc_driver_dithered_alpha_to_coverage 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_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 80b56997d2..3ca5d216dc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -117,6 +117,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.alphaToCoverage = product->state.alphaToCoverage; if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0; + product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage; return product; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index a456ce969f..40b6eeef9d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -72,6 +72,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); + mix(pixel.ditheredAlphaToCoverage); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 10297ffef8..6692c891cd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -139,7 +139,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4ac57ebdca..b3e056cd72 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -114,6 +114,7 @@ struct State { bool negativeOneToOne; bool depthClamp = false; bool alphaToCoverage = false; + bool ditheredAlphaToCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b467252ece..cebcfc53ff 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -130,6 +130,27 @@ constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x0001 constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; +constexpr std::uint32_t AlphaToCoverageDithered = 0x00018701u; + +bool alphaToCoverageDisabled(const Registers& cx) { + const auto control = find(cx, 0x203); + return control != cx.end() && (control->second & 0x800u) != 0; +} + +bool ditheredAlphaSamplesSupported(const Registers& cx) { + const auto config = find(cx, 0x2f8); + const auto eqaa = find(cx, 0x201); + return config != cx.end() && (config->second & 0x700007u) == 0x300003u && eqaa != cx.end() && (eqaa->second & 0x207777u) == 0x3303u; +} + +bool ditheredAlphaLocationsSupported(const Registers& cx) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto first = find(cx, 0x2fe + pixel * 4u); + const auto second = find(cx, 0x2ff + pixel * 4u); + if (first == cx.end() || second == cx.end() || first->second != 0xbd153fd1u || second->second != 0x9773f95bu) return false; + } + return true; +} constexpr std::uint32_t ScanModeMask = ~0x22u; constexpr std::uint32_t ScanControlMask = ~0x76023fffu; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; @@ -358,7 +379,7 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { const auto offset = 0x2feu + pixel * 4u; if (const auto locations = word(cx, offset); locations && (*locations & 0xffu) != 0) return zeroMessage(offset, *locations, "MSAA_ENABLE on a single-sample target whose sample is off the pixel center"); } - if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*alphaToMask == AlphaToCoverageDithered && alphaToCoverageDisabled(cx))) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; const auto stages = word(cx, 0x2d5); @@ -767,8 +788,15 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); - result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; - if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); + const auto alpha = read(cx, 0x2dc); + const bool alphaDisabled = alpha == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + result.alphaToCoverage = alpha == AlphaToCoverageUndithered || (!alphaDisabled && alpha == AlphaToCoverageDithered); + result.ditheredAlphaToCoverage = result.alphaToCoverage && alpha == AlphaToCoverageDithered; + if (!result.alphaToCoverage) zero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); + if (result.ditheredAlphaToCoverage) { + Require(ditheredAlphaSamplesSupported(cx), "dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"); + Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); + } Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); @@ -988,6 +1016,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto mrt0 = std::find_if(result.colors.begin(), result.colors.end(), [](const ColorTarget& color) { return color.exportIndex == 0; }); const auto format = read(cx, 0x1c5) & 0xfu; Require(format == 3u || format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); + Require(!result.ditheredAlphaToCoverage || format == 4u || format == 9u, "dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"); Require(mrt0 == result.colors.end() || (mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None), "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); } APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast(result.topology)); @@ -1319,8 +1348,21 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t zFormat = 0; static_cast(value(cx, 0x1c4, zFormat)); if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; - if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { - if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + if (value(cx, 0x2dc, word)) { + const bool alphaDisabled = word == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + if ((word != AlphaToCoverageUndithered && word != AlphaToCoverageDithered) || alphaDisabled) { + if (auto reason = nonzero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + } + if (!alphaDisabled && word == AlphaToCoverageDithered && !ditheredAlphaSamplesSupported(cx)) return "AGC graphics: dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"; + if (!alphaDisabled && word == AlphaToCoverageDithered) { + if (!ditheredAlphaLocationsSupported(cx)) return "AGC graphics: dithered alpha-to-coverage with custom sample locations is unsupported"; + std::uint32_t format = 0; + if (value(cx, 0x1c5, format)) { + format &= 0xfu; + if (format != 4u && format != 5u && format != 6u && format != 9u) return "AGC graphics: alpha-to-coverage needs an MRT0 export format with a floating-point alpha"; + if (format != 4u && format != 9u) return "AGC graphics: dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"; + } + } } if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e54c2402ee..b55e53b76f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -631,9 +631,15 @@ void MsaaEnableSingleSampleTests() { queue.context.erase(0x306); expectFailure([&] { DecodeState(queue); }, "missing register"); queue = enabled(); + queue.context[0x203] &= ~0x800u; queue.context[0x2dc] = 0xaa01; refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); queue = enabled(); + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue.context[0x2dc] = 0x18701; + Require(!DecodeState(queue).alphaToCoverage && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target ignored the dithered alpha disable override"); + queue = enabled(); queue.context[0x313] = 0x6001; refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); for (const auto primitive : {2u, 7u, 17u}) { @@ -653,6 +659,7 @@ void MsaaEnableSingleSampleTests() { queue.context[0x292] = 7; refused(queue, "scan conversion mode"); clear.context[0x2fe] = 0x44; + clear.context[0x203] &= ~0x800u; clear.context[0x2dc] = 0xaa01; Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); } @@ -2639,14 +2646,86 @@ void DepthClipTests() { } } -void AlphaToCoverageTests() { +void UnditheredAlphaToCoverageTests() { auto queue = makeState(); + queue.context[0x203] = 0x8010; + queue.context[0x2dc] = 0xaa01; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + for (const auto samples : {1u, 8u}) { + if (samples == 8) { + queue.context[0x201] = 0x00133303; + queue.context[0x2f8] = 0x0030e003; + queue.context[0x292] = 3; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + } + for (const auto disabled : {false, true}) { + queue.context[0x203] = 0x8010u | (disabled ? 0x800u : 0u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.alphaToCoverage && !state.ditheredAlphaToCoverage && state.samples == samples, "the existing undithered alpha-to-coverage state changed"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered alpha-to-coverage state"); + } + } +} + +void AlphaToCoverageTests() { + UnditheredAlphaToCoverageTests(); + auto dithered = makeState(); + dithered.context[0x203] = 0x8010; + dithered.context[0x201] = 0x00133303; + dithered.context[0x2dc] = 0x18701; + dithered.context[0x2f8] = 0x0030e003; + dithered.context[0x292] = 3; + dithered.context[0x31d] = 0x1b000; + dithered.context[0x1b3] = 2; + dithered.context[0x1b4] = 2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + dithered.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + dithered.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + const auto rejection = AgcDriver::Graphics::DrawRejection(dithered, false); + Require(rejection.empty(), "eight-sample default dither refused: " + rejection); + const auto ditheredState = AgcDriver::Graphics::DecodeState(dithered); + Require(ditheredState.alphaToCoverage && ditheredState.samples == 8, "default dither did not enable eight-sample coverage"); + Require(ditheredState.ditheredAlphaToCoverage, "default dither did not reach the shader state"); + for (const auto eqaa : {0x00133333u, 0x00132303u, 0x00133203u, 0x00333303u}) { + dithered.context[0x201] = eqaa; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "DB_EQAA"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("DB_EQAA") != std::string::npos, "the precheck accepted unmatched dithered coverage samples"); + } + dithered.context[0x201] = 0x00133303; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "custom sample locations"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("custom sample locations") != std::string::npos, "the precheck accepted unmeasured dithered sample locations"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + for (const auto format : {5u, 6u}) { + dithered.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "FP16_ABGR or 32_ABGR"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("FP16_ABGR or 32_ABGR") != std::string::npos, "the precheck accepted an unmeasured dithered export format"); + } + dithered.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(dithered).ditheredAlphaToCoverage, "FP16 default dither was refused"); + dithered.context[0x203] |= 0x800u; + Require(!AgcDriver::Graphics::DecodeState(dithered).alphaToCoverage, "the shader disable override was ignored"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).empty(), "the precheck ignored the shader disable override"); + dithered.context[0x2dc] |= 2u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "alpha-to-coverage"); + auto queue = makeState(); + queue.context[0x203] = 0; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; Require(!AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage decoded while disabled"); queue.context[0x2dc] = 0xaa01; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected undithered alpha-to-coverage"); + queue.context[0x203] |= 0x800u; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "the existing undithered single-sample state changed under the shader disable override"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered single-sample state under the shader disable override"); + queue.context[0x203] &= ~0x800u; for (const auto control : {0x8701u, 0x1aa01u, 0xaa03u}) { queue.context[0x2dc] = control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); @@ -2690,6 +2769,7 @@ void InitialContextTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_VTE_CNTL=0x"); } queue.context[0x206] = 0x43f; + queue.context[0x203] &= ~0x800u; queue.context[0x2dc] |= 1; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); queue.context.erase(0x2dc); @@ -5867,11 +5947,22 @@ int main(int argc, char** argv) { pipelineUse.unlock(); Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished"); } + if (argc == 2 && std::string_view(argv[1]) == "--undithered-alpha-to-coverage-state") { + UnditheredAlphaToCoverageTests(); + std::cout << "Existing undithered alpha-to-coverage states passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--alpha-to-coverage-state") { + MsaaEnableSingleSampleTests(); + AlphaToCoverageTests(); + std::cout << "Alpha-to-coverage state and refusal tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 0e2aa90eae..fd5b5f5b08 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1786,6 +1787,177 @@ std::string requestPrefix(std::string_view text, std::size_t bytes) { return result; } +void verifyCanonicalVersion20And21Requests() { + using namespace ShaderRecompiler; + struct Fixture { ShaderStage stage; std::uint32_t selector; bool subgroups; std::string_view text; }; + static constexpr std::array fixtures{{ + {ShaderStage::Fragment, 0u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 0u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 1u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 1u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 2u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 2u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 3u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 3u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Vertex, 330245u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 330245u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + {ShaderStage::Vertex, 656650u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 656650u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + }}; + constexpr std::array userData{0x13579bdfu, 0x2468ace0u, 0xdeadbeefu}; + constexpr std::array bytes{std::byte{0x12}, std::byte{0x34}, std::byte{0x56}, std::byte{0x78}}; + const RequestSerializer serializer; + // Original serializers at 4bb2c7f394 (20) and 075169f7b3 (21) emitted these + // twelve payloads independently; every pair differs only in its version header. + for (const auto version : {20u, 21u}) { + for (const auto& fixture : fixtures) { + auto text = std::string(fixture.text); + if (version == 21u) { + constexpr std::string_view prefix = "NVNQQRUAAAA"; + text.replace(0, prefix.size(), prefix); + } + const auto legacy = serializer.Deserialize(text); + const auto& request = legacy.request; + require(request.shader.stage == fixture.stage && request.shader.codeAddress == 0x30000u && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u, "canonical version20/21 shader identity was misread"); + require(request.context.waveSize == 64u && request.context.userDataBaseRegister == 8u && request.context.userData.size() == userData.size() && std::equal(request.context.userData.begin(), request.context.userData.end(), userData.begin()), "canonical version20/21 guest context was misread"); + require(request.context.memory.size() == 1u && request.context.memory[0].guestAddress == 0x50000u && request.context.memory[0].bytes.size() == bytes.size() && std::equal(request.context.memory[0].bytes.begin(), request.context.memory[0].bytes.end(), bytes.begin()), "canonical version20/21 trailing memory was misread"); + require(request.layout.descriptorSet == 2u && request.layout.firstBinding == 11u && request.layout.pushConstantOffsetBytes == 16u && request.layout.pushConstantSizeBytes == 64u && !request.useCache, "canonical version20/21 trailing layout or cache flag was misread"); + require(request.target.vertexSubgroups == fixture.subgroups && request.target.float64DenormPreserve && request.target.registrationModules && request.target.registrationWordLimit == 12345u && request.target.nonConstantImageOffsets && request.target.srgbDecodeFormats == 0x12345678u && request.target.narrowSubgroupClock, "canonical version20/21 target fields were misread"); + if (fixture.stage == ShaderStage::Fragment) { + require(request.context.pixel.has_value() && !request.context.vertex, "canonical version20/21 pixel presence was misread"); + const auto& pixel = *request.context.pixel; + require(pixel.rasterPrimitive == static_cast(fixture.selector) && !pixel.layerExported && !pixel.ditheredAlphaToCoverage && pixel.alphaToCoverage && pixel.stencilRefExportEnable && pixel.targetOutputMode[0] == 9u && pixel.targetExportMapping[0] == 0xe4u, "canonical version20/21 pixel fields were misread"); + } else { + require(request.context.vertex.has_value() && !request.context.pixel, "canonical version20/21 vertex presence was misread"); + const auto& vertex = *request.context.vertex; + require(vertex.paClVsOutCntl == fixture.selector && vertex.fetchAttribReg == 9u && vertex.fetchBufferReg == 13u && vertex.fetchEmbedded, "canonical version20/21 vertex fields were misread"); + } + const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); + require(RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(upgraded.request), "upgrading canonical version20/21 changed shader identity"); + require(serializer.Serialize(upgraded.request) == serializer.Serialize(request), "upgrading canonical version20/21 changed request fields"); + } + } +} void verifyPixelRequestSerialization() { using namespace ShaderRecompiler; const auto fields = [](const ShaderPixelStageInfo& value) { @@ -1794,7 +1966,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1839,6 +2011,7 @@ void verifyPixelRequestSerialization() { for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; pixel.stencilRefExportEnable = true; + pixel.ditheredAlphaToCoverage = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1863,6 +2036,10 @@ void verifyPixelRequestSerialization() { RecompileCacheKey::Build(changed, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(changed), "runtime pixel export mapping changed the static shader identity"); } + auto undithered = request; + undithered.context.pixel->ditheredAlphaToCoverage = false; + RecompileCacheKey::Build(undithered, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(undithered), "dithered coverage did not change shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -1883,15 +2060,121 @@ void verifyPixelRequestSerialization() { minimal.context.pixel = ShaderPixelStageInfo{}; minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; minimal.context.pixel->layerExported = true; + minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRgAAAA=", "new requests did not use serialization version 24"); - require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); + require(requestPrefix(encoded, 8u) == "NVNQQRkAAAA=", "new requests did not use serialization version 25"); + require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && serializer.Deserialize(encoded).request.context.pixel->ditheredAlphaToCoverage, "the pixel primitive class or dither mode did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); - constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 22u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class or layer export flag was accepted"); + const auto legacy16 = serializer.Deserialize( + "NVNQQRAAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAQAAAAgAAAAAAAAAAAAAAAAQ=="); + require(legacy16.request.context.pixel->alphaToCoverage && !legacy16.request.context.pixel->ditheredAlphaToCoverage, "version16 coverage acquired a dither mode"); + require(legacy16.request.context.pixel->targetOutputMode[0] == 9u && legacy16.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy16.request.target.float64DenormPreserve, "version16 export or FP64 fields were lost"); + require(legacy16.request.layout.firstBinding == 11u && legacy16.request.layout.pushConstantOffsetBytes == 16u && !legacy16.request.useCache, "version16 trailing fields were misaligned"); + const auto upgraded16 = serializer.Deserialize(serializer.Serialize(legacy16.request)); + require(RecompileCacheKey::ContextHash(legacy16.request) == RecompileCacheKey::ContextHash(upgraded16.request), "upgrading version16 changed shader identity"); + const auto legacy17 = serializer.Deserialize( + "NVNQQREAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAIAAAAAAAAAAAAAAAAEBOTAAAA=="); + require(legacy17.request.context.pixel->alphaToCoverage && legacy17.request.context.pixel->stencilRefExportEnable && !legacy17.request.context.pixel->ditheredAlphaToCoverage, "version17 coverage or stencil flags were lost"); + require(legacy17.request.target.float64DenormPreserve && legacy17.request.target.registrationModules && legacy17.request.target.registrationWordLimit == 12345u, "version17 registration or FP64 fields were lost"); + require(legacy17.request.context.pixel->targetOutputMode[0] == 9u && legacy17.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy17.request.layout.firstBinding == 11u && legacy17.request.layout.pushConstantOffsetBytes == 16u && !legacy17.request.useCache, "version17 export or trailing fields were misaligned"); + const auto upgraded17 = serializer.Deserialize(serializer.Serialize(legacy17.request)); + require(RecompileCacheKey::ContextHash(legacy17.request) == RecompileCacheKey::ContextHash(upgraded17.request), "upgrading version17 changed shader identity"); + const std::array, 2> legacy18Requests{{ + {false, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAA="}, + {true, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAE="} + }}; + for (const auto& [subgroups, text] : legacy18Requests) { + const auto legacy18 = serializer.Deserialize(text); + require(legacy18.request.target.vertexSubgroups == subgroups, "version18 vertex-subgroup flag was lost"); + require(legacy18.request.context.pixel->alphaToCoverage && legacy18.request.context.pixel->stencilRefExportEnable && !legacy18.request.context.pixel->ditheredAlphaToCoverage, "version18 coverage or stencil flags were lost"); + require(legacy18.request.target.float64DenormPreserve && legacy18.request.target.registrationModules && legacy18.request.target.registrationWordLimit == 12345u, "version18 registration or FP64 fields were lost"); + require(legacy18.request.context.pixel->targetOutputMode[0] == 9u && legacy18.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy18.request.layout.firstBinding == 11u && legacy18.request.layout.pushConstantOffsetBytes == 16u && !legacy18.request.useCache, "version18 export or trailing fields were misaligned"); + const auto upgraded18 = serializer.Deserialize(serializer.Serialize(legacy18.request)); + require(RecompileCacheKey::ContextHash(legacy18.request) == RecompileCacheKey::ContextHash(upgraded18.request), "upgrading version18 changed shader identity"); + require(upgraded18.request.target.vertexSubgroups == subgroups, "upgrading version18 changed vertex-subgroup support"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRkAAAA="}) { + const std::array, std::string_view>, 8> legacy19Requests{{ + {{static_cast(0u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(0u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(1u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(1u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(2u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(2u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(3u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(3u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + }}; + for (const auto& [flags, text] : legacy19Requests) { + const auto legacy19 = serializer.Deserialize(text); + require(legacy19.request.context.pixel->rasterPrimitive == flags.first && !legacy19.request.context.pixel->ditheredAlphaToCoverage, "version19 primitive or dither mode was misread"); + require(legacy19.request.context.pixel->alphaToCoverage && legacy19.request.context.pixel->stencilRefExportEnable, "version19 coverage or stencil flags were lost"); + require(legacy19.request.target.vertexSubgroups == flags.second && legacy19.request.target.float64DenormPreserve && legacy19.request.target.registrationModules && legacy19.request.target.registrationWordLimit == 12345u, "version19 target fields were misaligned"); + require(legacy19.request.context.pixel->targetOutputMode[0] == 9u && legacy19.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy19.request.layout.firstBinding == 11u && legacy19.request.layout.pushConstantOffsetBytes == 16u && !legacy19.request.useCache, "version19 export or trailing fields were misaligned"); + const auto upgraded19 = serializer.Deserialize(serializer.Serialize(legacy19.request)); + require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); + } + constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; + for (std::size_t bytes = 0; bytes < 23u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export or dither flag was accepted"); + } + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -1980,11 +2263,11 @@ void verifyVertexInfoVersion19() { while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset; require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); - auto version20 = flagged; - version20[4] = 20u; - const auto replay20 = serializer.Deserialize(encodeBase64(version20)); - require(replay20.request.context.vertex.has_value() && replay20.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a version 20 vertex info lost its position export layout"); - require(serializer.Serialize(replay20.request) == marked, "a version 20 vertex info misread the fields after its position export layout"); + auto version20Vertex = flagged; + version20Vertex[4] = 20u; + const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); + require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); + require(serializer.Serialize(replay20Vertex.request) == marked, "a main version20 vertex request misread trailing fields"); vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu; const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); @@ -2009,11 +2292,13 @@ void verifyVertexInfoVersion19() { std::size_t flagOffset = 0; while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset; require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset)); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 2u)); for (const auto version : {19u, 20u, 21u, 22u}) { - legacyPixel[4] = static_cast(version); - const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel)); - require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported, "a main version 19 to 22 request lost its pixel primitive class or read a layer export flag"); + auto versionPixel = legacyPixel; + versionPixel[4] = static_cast(version); + if (version < 22u) versionPixel.pop_back(); + const auto replayPixel = serializer.Deserialize(encodeBase64(versionPixel)); + require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage, "a main version 19 to 22 request lost its pixel primitive class or read a layer export or dither flag"); require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class"); } } @@ -2102,7 +2387,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); - require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage"); + require(!pixel.alphaToCoverage && !pixel.ditheredAlphaToCoverage, "a legacy request enabled alpha-to-coverage"); require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); require(pixel.rasterPrimitive == RasterPrimitive::Triangle, "a legacy request drew something other than triangles"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); @@ -3626,6 +3911,7 @@ int main(int argc, char** argv) { verifySingleLaneVertexWave(); verifyMeshConfiguration(); verifyPixelInputs(); + verifyCanonicalVersion20And21Requests(); verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); verifyPositionExportComponents(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 610161bced..dea1ea5f19 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" @@ -29,9 +30,12 @@ #include #include #include +#include +#include #include #include #include +#include #include namespace { @@ -65,11 +69,11 @@ constexpr std::array Destinations{{ {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} }}; -alignas(256) std::array, 8> Programs{}; +alignas(256) std::array, 16> Programs{}; std::size_t programCount = 0; std::uint16_t Half(float value) { - Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + Require(value == 0.0f || (value >= 0.0078125f && value <= 1.0f), "the test's half values are normal and at most 1"); if (value == 0.0f) return 0; const auto bits = std::bit_cast(value); Require((bits & 0x1fffu) == 0, "the test's half values are exact"); @@ -93,7 +97,7 @@ std::uint32_t Packed16(float value, std::uint32_t exportFormat) { return static_cast(static_cast(std::lround(value * 32767.0f))); } -std::span Program(const Color& color, std::uint32_t exportFormat) { +std::span Program(const Color& color, std::uint32_t exportFormat, std::optional sampleMask = std::nullopt, std::optional packedAlpha = std::nullopt) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; @@ -101,9 +105,14 @@ std::span Program(const Color& color, std::uint32_t exportF code[at++] = 0x7e0002ffu | (vgpr << 17u); code[at++] = value; }; + if (sampleMask) { + move(8, *sampleMask); + code[at++] = 0xf8001084u; + code[at++] = 0x00080000u; + } if (exportFormat != Float32Export) { move(4, Packed16(color[0], exportFormat) | (Packed16(color[1], exportFormat) << 16u)); - move(5, Packed16(color[2], exportFormat) | (Packed16(color[3], exportFormat) << 16u)); + move(5, Packed16(color[2], exportFormat) | ((packedAlpha ? *packedAlpha : Packed16(color[3], exportFormat)) << 16u)); code[at++] = 0xf8001c0fu; code[at++] = 0x00000504u; } else { @@ -173,21 +182,23 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t samples = 1) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; cx[0x31c] = Float16x4Info; - cx[0x31d] = 0; + const auto log2 = static_cast(std::countr_zero(samples)); + cx[0x31d] = (log2 << 12u) | (log2 << 15u); + cx[0x321] = 0; cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); cx[0x3b8] = TiledAttrib3; cx[0x390] = static_cast(block.Address() >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.bytes == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.samples == samples && color.bytes * samples == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode, bool dithered = false, bool sampleMaskExport = false) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; const auto target = device.Target(); @@ -223,12 +234,15 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge state.blend = blend; state.blendConstants = {}; state.alphaToCoverage = alphaToCoverage; + state.ditheredAlphaToCoverage = dithered; + state.samples = color.samples; const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, sampleMaskExport ? 0x100u : 0u}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; auto prepared = AgcDriver::Graphics::DecodePixelStageInfo(context, {}); prepared.alphaToCoverage = alphaToCoverage; + prepared.ditheredAlphaToCoverage = dithered; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, {waveSize, 0, {}, std::nullopt, prepared, std::nullopt, pixelMemory}, @@ -241,6 +255,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge fragment.shader.code = snapshot.code; fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->alphaToCoverage = state.alphaToCoverage; + fragment.context.pixel->ditheredAlphaToCoverage = state.ditheredAlphaToCoverage; const ShaderRecompiler::SrtRuntime runtime{}; const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); @@ -348,12 +363,108 @@ void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTarge } } +void DitheredCoverage(AgcDriver::VulkanDevice& device) { + struct Case { + std::uint32_t bits; + std::uint32_t format; + std::array masks; + }; + constexpr std::array cases{{ + {0x00000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x80000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c7fffffu, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c800000u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3c800001u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3cffffffu, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3d000000u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3d000001u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3effffffu, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000000u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000001u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f77ffffu, 9u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x3f780000u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f780001u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x7f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xff800000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7fc00000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00000000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00008000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x000023ffu, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00002400u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002401u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x000027ffu, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002800u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x00002801u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x000037ffu, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003800u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003801u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003bbfu, 4u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x00003bc0u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003bc1u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x00007c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x0000fc00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + }}; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + for (const auto& test : cases) { + blocks.push_back(std::make_unique(layout.Bytes() * 8u)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + const auto multisampled = DecodeTarget(source, 8); + for (std::uint32_t sample = 0; sample < 8; ++sample) { + const Color color{std::ldexp(1.0f, static_cast(sample) - 7), 0, 0, 1}; + Draw(device, multisampled, false, Float32Export, Program(color, Float32Export, 1u << sample), false, true); + } + const auto alpha = test.format == Float32Export ? std::bit_cast(test.bits) : 0.0f; + const Color color{0, 0, 0, alpha}; + const auto half = test.format == Float16Export ? std::optional(static_cast(test.bits)) : std::nullopt; + Draw(device, multisampled, true, test.format, Program(color, test.format, std::nullopt, half), true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = multisampled; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = test.masks[2u * (x & 1u) + (y & 1u)]; + const auto expected = x < ScissorWidth ? static_cast(255u - mask) / 1024.0f : 0.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u]); + if (actual == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), "dithered coverage export %u alpha 0x%08x pixel (%u,%u): resolved %.9g, expected %.9g for mask0x%02x", test.format, test.bits, x, y, actual, expected, mask); + throw std::runtime_error(detail); + } + } + programCount = 0; + } + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); } -int main() { +} + +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { + try { + DitheredCoverage(*device); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, eight-sample color attachments are unavailable: %s\n", error.what()); + return VulkanTestSkipped; + } + std::puts("dithered eight-sample alpha-to-coverage tests passed"); + return 0; + } + Require(argc == 1, "unknown alpha-to-coverage test argument"); const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); Block block(layout.Bytes()); struct Case { diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 137ca7b8fd..33e899a818 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -182,6 +182,7 @@ private: append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); + append(key, value.ditheredAlphaToCoverage); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index ad4a3a04d0..93b1024051 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -338,6 +338,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.stencilRefExportEnable); writer.WriteU8(static_cast(info.rasterPrimitive)); writer.WriteBool(info.layerExported); + writer.WriteBool(info.ditheredAlphaToCoverage); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -389,6 +390,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); if (version >= 23u) info.layerExported = reader.ReadBool(); + if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -760,7 +762,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(24u); + writer.WriteU32(25u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -792,7 +794,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 24u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 25u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 9c521c43ea..77e52d36de 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -177,6 +177,7 @@ struct ShaderPixelInputInfo { ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; + bool psDitheredAlphaToCoverage = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 419c219764..6ded41848d 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -236,7 +236,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput()) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 753fe7525c..f17e0ad63b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -152,6 +152,7 @@ struct ShaderPixelStageInfo { bool stencilRefExportEnable; RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; + bool ditheredAlphaToCoverage = false; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index aa0c23be9c..395e365f6e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -288,7 +288,50 @@ void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { state.module.AddFunction(spv::OpStore, pointer, mask); } +void StoreDitheredAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + constexpr std::array masks{{ + 0x00000000u, 0x00000010u, 0x00000004u, 0x10000004u, 0x10000002u, 0x10100002u, 0x04100002u, 0x04101002u, + 0x04101001u, 0x04101011u, 0x04041011u, 0x14041011u, 0x12041011u, 0x12141011u, 0x12140411u, 0x12141411u, + 0x12141411u, 0x12141431u, 0x12141415u, 0x32141415u, 0x32121415u, 0x32321415u, 0x16321415u, 0x16323415u, + 0x15323415u, 0x15323435u, 0x15163435u, 0x35163435u, 0x35163235u, 0x35363235u, 0x35361635u, 0x35363635u, + 0x35363635u, 0x35363675u, 0x3536363du, 0x7536363du, 0x75363637u, 0x75763637u, 0x3d763637u, 0x3d767637u, + 0x3d757637u, 0x3d757677u, 0x3d3d7677u, 0x7d3d7677u, 0x773d7677u, 0x777d7677u, 0x777d3e77u, 0x777d7e77u, + 0x777d7e77u, 0x777d7ef7u, 0x777d7e7fu, 0xf77d7e7fu, 0xf7777e7fu, 0xf7f77e7fu, 0x7ff77e7fu, 0x7ff7fe7fu, + 0x7ff7fd7fu, 0x7ff7fdffu, 0x7f7ffdffu, 0xff7ffdffu, 0xff7ff7ffu, 0xfffff7ffu, 0xffff7fffu, 0xffffffffu, + }}; + const auto alpha = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); + const auto positive = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), alpha, ConstantF32Value(state, 0.0f)), alpha, ConstantF32Value(state, 0.0f)); + const auto bounded = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), positive, ConstantF32Value(state, 63.0f / 64.0f)), ConstantF32Value(state, 63.0f / 64.0f), positive); + const auto index = Unary(state, spv::OpConvertFToU, TypeU32(state), Binary(state, spv::OpFMul, TypeF32(state), bounded, ConstantF32Value(state, 64.0f))); + std::array words{}; + for (std::size_t i = 0; i < words.size(); ++i) words[i] = ConstantU32(state, masks[i]); + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); + const auto table = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, array), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, table, state.module.Constant(spv::OpConstantComposite, array, std::span(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeU32(state)), pointer, table, index); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), packed, pointer); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), coord, InputVariableForKind(state, StageInputKind::FragCoord)); + std::array parity{}; + for (std::uint32_t i = 0; i < 2; ++i) { + const auto component = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, coord, i); + parity[i] = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpConvertFToU, TypeU32(state), component), ConstantU32(state, 1u)); + } + const auto pixel = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpShiftLeftLogical, TypeU32(state), parity[0], ConstantU32(state, 1u)), parity[1]); + const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pixel, ConstantU32(state, 3u)); + const auto mask = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), packed, shift), ConstantU32(state, 255u)); + StoreCoverage(state, Unary(state, spv::OpBitcast, TypeI32(state), mask)); +} + void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + if (state.inputInfo.pixel->psDitheredAlphaToCoverage) { + StoreDitheredAlphaCoverage(state, color); + return; + } const auto alpha = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); const auto below = state.module.AllocateId(); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 7fbb203fa3..cad2628806 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -142,7 +142,9 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; pixelStorage.psConservativeZExport = pixel.conservativeZExport; pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; + if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; + pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 18b926c5ec..c60d90b112 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -1,8 +1,13 @@ #include "ControlFlow/RequestSerializer.hpp" +#include +#include +#include #include #include +#include #include +#include namespace { @@ -10,13 +15,70 @@ constexpr char kHistoricalVersion21[] = "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; constexpr char kVersion21WithF16Tail[] = "NVNQQRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAEB"; + constexpr char kVersion22WithoutF16Tail[] = "NVNQQRYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAE="; +constexpr char kLayerVersion23[] = + "NVNQQRcAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAA="; + void Require(bool condition) { if (!condition) std::abort(); } +std::vector DecodeBase64(std::string_view text) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector bytes; + std::uint32_t chunk = 0; + std::size_t bits = 0; + for (const char symbol : text) { + if (symbol == '=') break; + const auto value = alphabet.find(symbol); + Require(value != std::string_view::npos); + chunk = ((chunk << 6u) | static_cast(value)) & 0xffffffu; + bits += 6u; + if (bits >= 8u) { + bits -= 8u; + bytes.push_back(static_cast((chunk >> bits) & 0xffu)); + } + } + return bytes; +} + +std::string EncodeBase64(const std::vector& bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string text; + for (std::size_t index = 0; index < bytes.size(); index += 3u) { + const std::size_t count = std::min(3u, bytes.size() - index); + std::uint32_t chunk = static_cast(bytes[index]) << 16u; + if (count > 1u) chunk |= static_cast(bytes[index + 1u]) << 8u; + if (count > 2u) chunk |= bytes[index + 2u]; + for (std::size_t symbol = 0; symbol < 4u; ++symbol) { + text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '='); + } + } + return text; +} + } int main() { @@ -36,14 +98,325 @@ int main() { } Require(rejectedTruncatedVersion22); - ShaderRecompiler::RecompileRequest request{}; - request.target.float16DenormPreserve = false; - const auto withoutPreserve = serializer.Deserialize(serializer.Serialize(request)); - Require(!withoutPreserve.request.target.float16DenormPreserve); + struct CanonicalMain22 { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + constexpr std::array canonicalMain22{{ + {true, false, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkA" + "AAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {true, true, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIA" + "AAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, false, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, true, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAA" + "AAABeFY0EgEBATkwAAAAAA==" + }, + }}; + for (const auto& fixture : canonicalMain22) { + for (const bool preserve : {false, true}) { + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(request.context.vertex->fetchAttribReg == 9u && request.context.vertex->fetchBufferReg == 13u && request.context.vertex->fetchEmbedded && request.context.vertex->paClVsOutCntl == 0x00050a05u); + Require(!fixture.pixel || (request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle && request.context.pixel->alphaToCoverage && request.context.pixel->stencilRefExportEnable && request.context.pixel->targetOutputMode[0] == 9u && request.context.pixel->targetExportMapping[0] == 0xe4u && !request.context.pixel->layerExported && !request.context.pixel->ditheredAlphaToCoverage)); + Require(request.graphics.has_value() && request.graphics->firstUserSgpr == 6u && request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->domain == 2u && request.graphics->tessellation->outputTopology == 3u)); + Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); + Require(request.target.float16DenormPreserve == preserve); + auto upgraded = bytes; + upgraded[4] = 25u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u}); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); + Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + auto truncated = bytes; + truncated.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + if (!fixture.pixel) { + auto vertex23 = bytes; + vertex23[4] = 23u; + const auto vertexReplay = serializer.Deserialize(EncodeBase64(vertex23)); + Require(serializer.Serialize(vertexReplay.request) == EncodeBase64(upgraded)); + continue; + } + for (const bool layer : {false, true}) { + auto layered = bytes; + layered[4] = 23u; + layered.insert(layered.begin() + 258u, static_cast(layer)); + const auto layerReplay = serializer.Deserialize(EncodeBase64(layered)); + Require(layerReplay.request.context.pixel->layerExported == layer && !layerReplay.request.context.pixel->ditheredAlphaToCoverage); + Require(layerReplay.request.target.float16DenormPreserve == preserve && layerReplay.request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || layerReplay.request.graphics->tessellation->patchesPerGroup == 63u); + auto truncatedLayer = layered; + truncatedLayer.pop_back(); + bool layerRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncatedLayer))); + } catch (const std::runtime_error& error) { + layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(layerRejected); + layered[4] = 25u; + layered.insert(layered.begin() + 259u, 0u); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); + Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); + } + } + } - request.target.float16DenormPreserve = true; - const auto withPreserve = serializer.Deserialize(serializer.Serialize(request)); - Require(withPreserve.request.target.float16DenormPreserve); + const auto layer23 = DecodeBase64(kLayerVersion23); + Require(layer23.size() == 1451u); + for (const bool layer : {false, true}) { + for (const bool preserve : {false, true}) { + auto bytes = layer23; + bytes[258] = static_cast(layer); + bytes.back() = static_cast(preserve); + const auto encoded23 = EncodeBase64(bytes); + const auto replay = serializer.Deserialize(encoded23); + Require(replay.request.context.pixel.has_value() && replay.request.context.vertex.has_value()); + Require(replay.request.context.pixel->layerExported == layer); + Require(!replay.request.context.pixel->ditheredAlphaToCoverage); + Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); + Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); + Require(replay.request.target.float16DenormPreserve == preserve); + bytes[4] = 25u; + bytes.insert(bytes.begin() + 259u, 0u); + Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); + + auto truncated23 = DecodeBase64(encoded23); + truncated23.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated23))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } + } + + struct Main24Fixture { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + static constexpr std::array canonicalMain24{{ + {true, false, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAE=" + }, + {true, true, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAABBAAAAAQAAAAC" + "AAAAAgAAAAMAAAA/AAAAJQAAAAEANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, false, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, true, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAlAAAAAQA0782rAAAAOQAAAAQA" + "AAAJAAAAAAF4VjQSAQEBOTAAAAAB" + }, + }}; + for (const auto& fixture : canonicalMain24) { + for (const bool preserve : {false, true}) { + for (const bool layer : {false, true}) { + if (!fixture.pixel && layer) continue; + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + if (fixture.pixel) bytes[258] = static_cast(layer); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(!fixture.pixel || (request.context.pixel->layerExported == layer && !request.context.pixel->ditheredAlphaToCoverage)); + Require(request.graphics.has_value() && request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->primitiveGroupSize == 37u && request.graphics->tessellation->breakAtInstance)); + Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); + Require(request.target.float16DenormPreserve == preserve); + auto upgraded = bytes; + upgraded[4] = 25u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, 0u); + Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + } + } + } + + ShaderRecompiler::RecompileRequest request{}; + for (const bool preserve : {false, true}) { + request.target.float16DenormPreserve = preserve; + const auto encoded = serializer.Serialize(request); + const auto replay = serializer.Deserialize(encoded); + Require(replay.request.target.float16DenormPreserve == preserve); + auto bytes = DecodeBase64(encoded); + Require(bytes.size() >= 8u && bytes[4] == 25u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto future = bytes; + future[4] = 26u; + bool futureRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(future))); + } catch (const std::runtime_error& error) { + futureRejected = std::string_view(error.what()) == "unsupported recompile request serialization version"; + } + Require(futureRejected); + bytes.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(bytes))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } return 0; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3a86e84519..ae3942130d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -7,7 +7,7 @@ - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [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. +- [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 - [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 From 246d16a035c858ebfed4325da80532faba7e7914 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 13:01:48 +0300 Subject: [PATCH 339/353] test(agc): preserve targetless alpha exports after rebase --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cebcfc53ff..955504eccb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1359,7 +1359,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t format = 0; if (value(cx, 0x1c5, format)) { format &= 0xfu; - if (format != 4u && format != 5u && format != 6u && format != 9u) return "AGC graphics: alpha-to-coverage needs an MRT0 export format with a floating-point alpha"; + if (format != 3u && format != 4u && format != 5u && format != 6u && format != 9u) return "AGC graphics: alpha-to-coverage needs an MRT0 export format with a floating-point alpha"; if (format != 4u && format != 9u) return "AGC graphics: dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b55e53b76f..e182179826 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2702,7 +2702,7 @@ void AlphaToCoverageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "custom sample locations"); Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("custom sample locations") != std::string::npos, "the precheck accepted unmeasured dithered sample locations"); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; - for (const auto format : {5u, 6u}) { + for (const auto format : {3u, 5u, 6u}) { dithered.context[0x1c5] = format; expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "FP16_ABGR or 32_ABGR"); Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("FP16_ABGR or 32_ABGR") != std::string::npos, "the precheck accepted an unmeasured dithered export format"); From bfca92e8aecff3c01c0e237ae8220f68840477c9 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 14 Oct 2026 23:07:26 +1300 Subject: [PATCH 340/353] test(agc): take the LDS update check's words as a span Retained SPIR-V is a span on main since #4806. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 52ff191b46..6eaeb3d5b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3208,7 +3208,7 @@ void verifyFunctionLdsUpdates() { request.useCache = false; return Recompile(request); }; - const auto violations = [](const std::vector& words) { + const auto violations = [](std::span words) { std::map storage; std::map constants; std::map types; From 2d3c9b4b1d4189402fbc86d71f5f0e7092b98b09 Mon Sep 17 00:00:00 2001 From: Nesyth <18372752+Nesyth@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:24:41 +0200 Subject: [PATCH 341/353] perf(agc): read DCC keys from the pending GPU fills that cover them instead of waiting for the fills --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../Graphics/src/DccMetadata.cpp | 89 +++++++++++-------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 33 +++++++ .../tests/execution/DccKeysFirstWrite.cpp | 25 +++++- 4 files changed, 115 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 477b211c16..4270dc5a81 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1015,6 +1015,12 @@ if(BUILD_TESTING) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_pending_key_stores COMMAND agc_driver_recorder_tests --pending-key-stores-only) + set_tests_properties(agc_pending_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_pending_key_stores PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index e1b0ba0099..b07b12c00d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -293,6 +293,48 @@ void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys) { namespace { +struct LandedKeys { + int value = -1; + bool mixed = false; + bool unknown = false; +}; + +LandedKeys KeysAfterPendingWrites(const Recorder& recorder, std::uint64_t begin, std::size_t count) { + auto writes = recorder.PendingWritesOver(begin, count); + std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); + std::vector> unwritten{{begin, begin + count}}; + LandedKeys landed; + const auto see = [&](int key) { + if (landed.value < 0) landed.value = key; + else if (landed.value != key) landed.mixed = true; + }; + for (const auto& write : writes) { + if (landed.mixed || unwritten.empty()) break; + std::vector> rest; + bool covered = false; + for (const auto& [from, to] : unwritten) { + if (write.end <= from || to <= write.begin) { + rest.emplace_back(from, to); + continue; + } + covered = true; + if (from < write.begin) rest.emplace_back(from, write.begin); + if (write.end < to) rest.emplace_back(write.end, to); + } + unwritten = std::move(rest); + if (!covered) continue; + if (write.value >= 0) see(write.value); + else landed.unknown = true; + } + for (const auto& [from, to] : unwritten) { + if (landed.mixed) break; + const auto* keys = reinterpret_cast(from); + if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); + else landed.mixed = true; + } + return landed; +} + std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, std::size_t count) { const auto end = begin + count; std::uint64_t note = 0; @@ -308,8 +350,13 @@ std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, break; } } - if (note == 0 || AnyShadowedOverlaps(begin, count) || recorder.NewestWriteNote(begin, count) != note) return std::nullopt; - return keys; + if (AnyShadowedOverlaps(begin, count)) return std::nullopt; + if (note != 0 && recorder.NewestWriteNote(begin, count) == note) return keys; + if (!GuestMemory::Accessible(reinterpret_cast(begin), count)) return std::nullopt; + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + if (landed.mixed) return DccKeys::Mixed; + if (landed.unknown || landed.value < 0) return std::nullopt; + return ByteKeys(static_cast(landed.value)); } // Whether the keys already read as uncompressed: scans the same bytes as ReadDccKeys, so it counts @@ -465,12 +512,14 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized return uniform ? ByteKeys(first) : DccKeys::Mixed; } +DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized); + DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) { memoized = false; if (resource.dccAddress == 0) return DccKeys::Uncompressed; DccKeys keys; if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known; - else keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); + else keys = currentDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys; if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; @@ -695,38 +744,8 @@ DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureResource& resource, std::size_t count) { const auto begin = resource.dccAddress; if (!GuestMemory::Accessible(reinterpret_cast(begin), count) || AnyShadowedOverlaps(begin, count)) return false; - auto writes = recorder.PendingWritesOver(begin, count); - std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); - std::vector> unwritten{{begin, begin + count}}; - int value = -1; - bool mixed = false; - const auto see = [&](int key) { - if (value < 0) value = key; - else if (value != key) mixed = true; - }; - for (const auto& write : writes) { - if (mixed || unwritten.empty()) break; - std::vector> rest; - bool covered = false; - for (const auto& [from, to] : unwritten) { - if (write.end <= from || to <= write.begin) { - rest.emplace_back(from, to); - continue; - } - covered = true; - if (from < write.begin) rest.emplace_back(from, write.begin); - if (write.end < to) rest.emplace_back(write.end, to); - } - unwritten = std::move(rest); - if (covered && write.value >= 0) see(write.value); - } - for (const auto& [from, to] : unwritten) { - if (mixed) break; - const auto* keys = reinterpret_cast(from); - if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); - else mixed = true; - } - return mixed || (value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(value))) == DccKeys::Uncompressed); + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + return landed.mixed || (landed.value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(landed.value))) == DccKeys::Uncompressed); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3415b5c76b..bd449f7847 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4613,7 +4613,31 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill did not read as its keys without a wait"); recorder.NotePendingWrite(address, 2 * keyCount); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later wider writer over a pending fill was not waited for"); + recorder.NotePendingFill(address, keyCount / 2, 0x40); + recorder.NotePendingFill(address + keyCount / 2, keyCount / 2, 0x40); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of pending 0001 fills over both halves did not read as 0001 without a wait"); + recorder.NotePendingWrite(address + keyCount / 2, 16); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later writer of unknown bytes over pending fills was not waited for"); + recorder.NotePendingFill(address, keyCount / 2, 0x00); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Mixed && recorder.PendingWriteOverlaps(address, keyCount), "a pending 0000 fill over half of uncompressed keys did not read as mixed without a wait"); recorder.Sync(); +#ifdef _WIN32 + auto* edgeBlock = static_cast(VirtualAlloc(nullptr, 2 * bytes, MEM_RESERVE, PAGE_NOACCESS)); + Require(edgeBlock != nullptr && VirtualAlloc(edgeBlock, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot reserve the key block past committed memory"); +#else + void* edgeMapping = mmap(nullptr, 2 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(edgeMapping != MAP_FAILED && mprotect(edgeMapping, bytes, PROT_READ | PROT_WRITE) == 0, "cannot reserve the key block past committed memory"); + auto* edgeBlock = static_cast(edgeMapping); +#endif + const auto edge = reinterpret_cast(edgeBlock) + bytes - keyCount / 2; + Require(!AgcDriver::GuestMemory::Accessible(reinterpret_cast(edge), keyCount), "keys past the end of committed memory read as accessible"); + recorder.NotePendingFill(edge, keyCount / 2, 0x40); + Require(CurrentDccKeys(edge, surfaceBytes) == DccKeys::Unreadable && !recorder.PendingWriteOverlaps(edge, keyCount), "keys running past the end of committed memory, with a pending fill over their committed half, were not read as unreadable after a wait"); +#ifdef _WIN32 + VirtualFree(edgeBlock, 0, MEM_RELEASE); +#else + munmap(edgeBlock, 2 * bytes); +#endif constexpr std::uint64_t displayBytes = 15u * 9u * 65536u; constexpr std::size_t consoleKeys = 49152; const auto displayKeys = DccKeyCount(TextureTileMode::kR64KBX, 4, 1920, 1080, displayBytes); @@ -6604,6 +6628,15 @@ int main(int argc, char** argv) { std::cout << "Color metadata pass tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pending-key-stores-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + pendingKeyStoreTests(device, recorder); + std::cout << "Pending DCC key store tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { drawInputInPlaceTests(device, recorder); std::cout << "In-place draw input tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 7de05243d1..e9668e78dc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -345,18 +345,37 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { } readPending(DccKeys::Uncompressed, false, "a pending fill of the keys with 0x5a, which is no clear code"); Require(fill(0, KeyBytes, 0x10), "the comp-to-single fill of the keys was not recorded"); - readPending(DccKeys::ClearSingle, true, "a pending comp-to-single fill of the keys"); + readPending(DccKeys::ClearSingle, false, "a pending comp-to-single fill of the keys"); + RequireKeys(keys, 0x10, "the keys after a pending comp-to-single fill landed"); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); - readPending(DccKeys::Clear0000, true, "pending 0000 fills over both halves of the keys"); + readPending(DccKeys::Clear0000, false, "pending 0000 fills over both halves of the keys"); + RequireKeys(keys, 0x00, "the keys after pending 0000 fills over both halves landed"); + Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x40), "the 0000 and 0001 fills of the key halves were not recorded"); + readPending(DccKeys::Uncompressed, false, "a pending 0000 fill and a pending 0001 fill over the halves of the keys"); Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half was not recorded"); Dispatch(device, ClearKeysCode, clearTailData, false); readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys and a kernel's 0000 store over the other half"); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill of the keys was not recorded"); + Dispatch(device, ClearKeysCode, clearTailData, false); + Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half over the kernel's store was not recorded"); + readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys, whose other half a kernel stored over an earlier 0001 fill"); Require(fill(0, KeyBytes, 0x10) && fill(0, KeyBytes, 0x00), "the 0x10 and 0000 fills of the keys were not recorded"); - readPending(DccKeys::Clear0000, true, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + readPending(DccKeys::Clear0000, false, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + RequireKeys(keys, 0x00, "the keys after a pending 0x10 fill and a pending 0000 fill landed"); + Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys under a kernel's store was not recorded"); + Dispatch(device, ClearKeysCode, clearData, false); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill over the kernel's store was not recorded"); + readPending(DccKeys::Clear0001, false, "a pending 0001 fill of the keys over a kernel's 0000 store over a pending 0x10 fill"); + RequireKeys(keys, 0x40, "the keys after a 0x10 fill, a kernel's 0000 store and a 0001 fill landed"); Require(fill(KeyBytes / 2, KeyBytes / 2, 0x10), "the 0x10 fill of the second key half was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00), "the 0000 fill of the first key half was not recorded"); readPending(DccKeys::Uncompressed, false, "a pending 0000 fill over half the keys whose other half holds 0x10"); + Require(fill(KeyBytes / 2, KeyBytes / 2, 0xc0), "the 1111 fill of the second key half was not recorded"); + device.WaitIdle(); + Require(fill(0, KeyBytes / 2, 0xc0), "the 1111 fill of the first key half was not recorded"); + readPending(DccKeys::Clear1111, false, "a pending 1111 fill over half the keys whose other half holds 1111"); + RequireKeys(keys, 0xc0, "the keys after a pending 1111 fill over a half that holds 1111 landed"); Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); From 838c1ca9612d91f72635fa3c5655a2eaeac882bb Mon Sep 17 00:00:00 2001 From: Nesyth <18372752+Nesyth@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:41:52 +0200 Subject: [PATCH 342/353] perf(agc): drop stale unit shadows over DCC keys so pending fills answer the keys without a GPU wait --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/DccMetadata.cpp | 5 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 104 ++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4270dc5a81..1b7a67457b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1017,8 +1017,10 @@ if(BUILD_TESTING) endif() add_test(NAME agc_pending_key_stores COMMAND agc_driver_recorder_tests --pending-key-stores-only) set_tests_properties(agc_pending_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_shadowed_key_stores COMMAND agc_driver_recorder_tests --shadowed-key-stores-only) + set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_pending_key_stores PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index b07b12c00d..ba5bb1d042 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -350,7 +350,10 @@ std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, break; } } - if (AnyShadowedOverlaps(begin, count)) return std::nullopt; + if (AnyShadowedOverlaps(begin, count)) { + PublishShadow(begin, count, PublishScope::Whole, PublishReason::Keys); + if (AnyShadowedOverlaps(begin, count)) return std::nullopt; + } if (note != 0 && recorder.NewestWriteNote(begin, count) == note) return keys; if (!GuestMemory::Accessible(reinterpret_cast(begin), count)) return std::nullopt; const auto landed = KeysAfterPendingWrites(recorder, begin, count); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index bd449f7847..c740eb7275 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4652,6 +4652,101 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +bool shadowedKeyStoreTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + if (!UnitShadowEnabled() || context.hostImportAlignment == 0) { + std::cout << "unit shadows or host imports unavailable: keys under a unit shadow not tested\n"; + return false; + } + constexpr std::uint64_t unit = 65536; + constexpr std::size_t bytes = 2 * unit; + constexpr std::size_t surfaceBytes = 65536; + constexpr std::size_t keyCount = surfaceBytes / 256; +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, unit); + Require(block != nullptr, "cannot reserve the shadowed key block"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(bytes), nullptr); + Require(section != nullptr, "cannot create the shadowed key block"); + GuestArena::GuestArenaMap_nid_postfix(block, bytes, section, 0, PAGE_READWRITE); + CloseHandle(section); +#else + void* block = AllocateWatched(bytes, unit); + Require(block != nullptr, "cannot allocate the shadowed key block"); +#endif + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* keys = static_cast(block); + std::memset(block, 0xff, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{context, block, address}; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || !Watched(address, bytes)) { + std::cout << "the shadowed key block is not imported and watched: keys under a unit shadow not tested\n"; + return false; + } + recorder.Sync(); + CollectWritesUncached(address, bytes); + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + const auto shadow = [&](std::uint8_t value) { + auto destination = ShadowDestinationFor(context, *import, address, address + unit); + Require(destination.has_value() && destination->seedUnits.empty(), "the key unit has no unit shadow destination"); + auto pattern = std::make_shared(context, static_cast(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memset(pattern->Bytes().data(), value, pattern->Bytes().size()); + const auto commands = recorder.Commands(); + recorder.Keep(pattern); + recorder.Keep(destination->slab); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const VkBufferCopy piece{0, destination->offset, unit}; + copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{address, address + unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), TrackerGeneration()); + Require(AnyShadowedOverlaps(address, keyCount), "the key unit is not shadowed"); + }; + recorder.NotePendingFill(address, unit, 0x00); + MarkWritten(address, unit); + shadow(0x40); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && !recorder.PendingWriteOverlaps(address, keyCount), "the keys under a fresh unit shadow were not read from its published bytes after a wait"); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40 && keys[unit] == 0xff && !AnyShadowedOverlaps(address, unit), "the fresh unit's publish did not land"); + recorder.Sync(); + shadow(0x22); + recorder.NotePendingFill(address, unit, 0xc0); + MarkWritten(address, unit); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear1111 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill over a stale unit shadow were not read as the fill's keys without a wait"); + Require(!AnyShadowedOverlaps(address, unit), "the stale unit still counts as shadowed after the key read"); + recorder.Sync(); + GuestTextureResource resource{}; + resource.baseAddress = address + unit; + resource.format = 56; + resource.dccAddress = address; + shadow(0x22); + recorder.NotePendingFill(address, unit, 0xc0); + MarkWritten(address, unit); + Require(!WaitForKeyWriters(resource, surfaceBytes).has_value() && recorder.PendingWriteOverlaps(address, keyCount), "the key writers over a stale unit shadow were waited for"); + recorder.Sync(); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40, "a stale unit's slab bytes were published"); + return true; +} + void sampleDumpTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0 || !context.bufferDeviceAddress) { @@ -6637,6 +6732,14 @@ int main(int argc, char** argv) { std::cout << "Pending DCC key store tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { + if (!shadowedKeyStoreTests(device, recorder)) { + std::cout << "skipped, unit shadows over a watched host import are unavailable\n"; + return 77; + } + std::cout << "DCC keys under unit shadows tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { drawInputInPlaceTests(device, recorder); std::cout << "In-place draw input tests passed\n"; @@ -6729,6 +6832,7 @@ int main(int argc, char** argv) { metadataPassTests(device, recorder); cmaskPassTests(device, recorder); pendingKeyStoreTests(device, recorder); + shadowedKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); copiedDirectOrderingTests(device, recorder); gpu.unlock(); From 955c60e32f0cc98753c894f75d63cb6e57ddd293 Mon Sep 17 00:00:00 2001 From: Dextroid17 Date: Mon, 12 Oct 2026 15:51:03 -0400 Subject: [PATCH 343/353] feat(tools): compile the changed translation units only --- CMakeLists.txt | 2 + tools/check_compile.py | 149 ++++++++++++++++++++++ tools/tests/test_check_compile.py | 197 ++++++++++++++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 tools/check_compile.py create mode 100644 tools/tests/test_check_compile.py diff --git a/CMakeLists.txt b/CMakeLists.txt index fe24138137..a5ec80bd05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py) + add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py) endif() endif() diff --git a/tools/check_compile.py b/tools/check_compile.py new file mode 100644 index 0000000000..1ec01a433c --- /dev/null +++ b/tools/check_compile.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import shlex +import shutil +import subprocess +import sys +from pathlib import Path + +CPP = (".cpp", ".cc", ".cxx", ".c") +DROP = ("-c", "-MD", "-MMD", "-MP") +DROP_WITH_VALUE = ("-o", "-MT", "-MF") + + +def capture(command, cwd=None): + return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", + errors="replace", cwd=cwd) + + +def repository(): + result = capture(["git", "rev-parse", "--show-toplevel"]) + if result.returncode != 0: + raise ValueError(result.stderr.strip() or "not a git checkout") + return Path(result.stdout.strip()) + + +def entries(build): + if not build.is_dir(): + raise ValueError(f"{build} is not a directory; configure the build first") + manifest = build / "compile_commands.json" + if manifest.is_file(): + return json.loads(manifest.read_text(encoding="utf-8")) + ninja = shutil.which("ninja") + if ninja is None: + raise ValueError(f"no {manifest} and ninja is not on PATH") + result = capture([ninja, "-C", str(build), "-t", "compdb"]) + if result.returncode != 0: + raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") + return json.loads(result.stdout) + + +def entry_key(entry): + directory = Path(entry["directory"]).resolve() + source = Path(entry["file"]) + if not source.is_absolute(): + source = directory / source + source = source.resolve() + common = Path(os.path.commonpath([str(directory), str(source)])) + return os.path.relpath(source, common).replace(os.sep, "/") + + +def index(build): + commands = {} + for entry in entries(build): + commands.setdefault(entry_key(entry), entry) + return commands + + +def changed(root, base): + result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", base]) + if result.returncode != 0: + raise ValueError(f"git diff {base}: {result.stderr.strip()}") + untracked = capture(["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "-z"]) + if untracked.returncode != 0: + raise ValueError(f"git ls-files: {untracked.stderr.strip()}") + names = dict.fromkeys(result.stdout.split("\0") + untracked.stdout.split("\0")) + return [name for name in names if name.endswith(CPP)] + + +def syntax_only(entry, source): + arguments = shlex.split(entry["command"]) + directory = Path(entry["directory"]) + wanted = source.resolve() + trimmed = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in DROP_WITH_VALUE: + index += 2 + continue + if argument in DROP: + index += 1 + continue + if not argument.startswith("-"): + candidate = Path(argument) + if not candidate.is_absolute(): + candidate = directory / candidate + if candidate.resolve() == wanted: + index += 1 + continue + trimmed.append(argument) + index += 1 + trimmed += ["-fsyntax-only", str(wanted)] + return trimmed + + +def check(root, build, files): + commands = index(build) + checked = 0 + failed = 0 + for file in files: + source = file.resolve() + name = os.path.relpath(source, root).replace(os.sep, "/") + entry = commands.get(name) + if entry is None: + print(f"{name}: no compile command in {build}", file=sys.stderr) + continue + checked += 1 + result = subprocess.run(syntax_only(entry, source), cwd=entry["directory"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + continue + failed += 1 + output = (result.stdout or "") + (result.stderr or "") + sys.stderr.write(output if output.endswith("\n") else output + "\n") + return checked, failed + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="compile the changed translation units only, to check a change builds without a full build") + parser.add_argument("--build", type=Path, default=Path("build"), + help="a configured Ninja build directory of this checkout (default: build)") + parser.add_argument("--base", default="origin/main", + help="branch the change is against (default: origin/main)") + parser.add_argument("paths", nargs="*", help="translation units to check instead of the changed ones") + args = parser.parse_args(argv) + try: + root = repository() + files = [Path(path) for path in args.paths] or [root / path for path in changed(root, args.base)] + if not files: + print(f"no changed translation unit against {args.base}") + return 0 + checked, failed = check(root, args.build, files) + except (json.JSONDecodeError, OSError, ValueError) as error: + parser.error(str(error)) + if failed: + print(f"{failed} of {checked} translation units did not compile", file=sys.stderr) + return 1 + if not checked: + print(f"no translation unit to check in {args.build}", file=sys.stderr) + return 1 + print(f"{checked} translation units compile") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py new file mode 100644 index 0000000000..bb79df647e --- /dev/null +++ b/tools/tests/test_check_compile.py @@ -0,0 +1,197 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_compile + +TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" + + +class CheckCompileTests(unittest.TestCase): + def setUp(self): + holder = tempfile.TemporaryDirectory() + self.addCleanup(holder.cleanup) + self.root = Path(holder.name) + subprocess.run(["git", "init", "-q", str(self.root)], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) + self.build = self.root / "build" + self.build.mkdir() + + def tool(self, *arguments, env=None): + environment = dict(os.environ) + environment.update(env or {}) + return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, + capture_output=True, text=True, env=environment) + + def source(self, relative, text="int x;\n"): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def compiler(self, body, name="compiler.py"): + script = self.root / name + script.write_text(body, encoding="utf-8") + return script + + def entry(self, relative, command): + return {"directory": str(self.build), "command": command, "file": str(self.root / relative)} + + def manifest(self, entries): + (self.build / "compile_commands.json").write_text(json.dumps(entries), encoding="utf-8") + + def failing(self, relative, message=None): + source = self.source(relative) + message = message if message is not None else f"{relative}:1:1: error: no\n" + script = self.compiler("import sys\nsys.stderr.write(%r)\nsys.exit(1)\n" % message) + self.manifest([self.entry(relative, f'"{sys.executable}" "{script}" "{source}"')]) + return source + + def test_a_translation_unit_that_does_not_compile_fails_with_the_compiler_output(self): + self.failing("unit.cpp") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + + def test_a_translation_unit_that_compiles_passes(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + def test_the_command_is_rewritten_to_syntax_only_without_object_or_dependency_files(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" -c -o unit.o ' + f'-MD -MT unit.o -MF unit.o.d "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_nothing_checked_is_a_failure(self): + self.manifest([]) + result = self.tool("orphan.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + self.assertIn("no translation unit to check in", result.stderr) + + def test_a_translation_unit_outside_the_build_is_reported_but_the_others_decide(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp", "orphan.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + + def test_an_absolute_path_names_the_same_translation_unit(self): + source = self.failing("unit.cpp") + result = self.tool(str(source)) + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + + def test_a_relative_source_argument_in_the_command_is_replaced(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + relative = os.path.relpath(source, self.build) + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" "{relative}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_a_build_directory_that_is_not_a_child_of_the_checkout_is_matched(self): + self.build = self.root / "out" / "build" + self.build.mkdir(parents=True) + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("--build", str(self.build), "unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + def test_a_changed_file_with_a_non_ascii_name_is_checked(self): + self.failing("caf\u00e9.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "caf\u00e9.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("caf\u00e9.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("error: no", result.stderr) + + def test_a_longer_path_does_not_shadow_the_translation_unit(self): + source = self.source("src/foo.cpp") + other = self.source("plugins/x/src/foo.cpp") + passes = self.compiler("import sys\nsys.exit(0)\n", name="passes.py") + fails = self.compiler( + "import sys\nsys.stderr.write('src/foo.cpp:1:1: error: no\\n')\nsys.exit(1)\n", name="fails.py") + self.manifest([ + self.entry("plugins/x/src/foo.cpp", f'"{sys.executable}" "{passes}" "{other}"'), + self.entry("src/foo.cpp", f'"{sys.executable}" "{fails}" "{source}"'), + ]) + result = self.tool("src/foo.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("src/foo.cpp:1:1: error: no", result.stderr) + + def test_a_new_untracked_translation_unit_is_checked(self): + self.source("base.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "base.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.failing("brandnew.cpp") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("brandnew.cpp:1:1: error: no", result.stderr) + + def test_a_missing_build_directory_is_rejected(self): + result = self.tool("--build", "nowhere", "unit.cpp") + self.assertEqual(result.returncode, 2) + self.assertIn("not a directory", result.stderr) + + def test_the_changed_translation_units_are_taken_from_the_base(self): + self.failing("changed.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "changed.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("changed.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("changed.cpp:1:1: error: no", result.stderr) + + def test_no_changed_translation_unit_is_not_a_failure(self): + self.source("clean.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "clean.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.manifest([]) + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 0) + self.assertIn("no changed translation unit", result.stdout) + + def test_the_commands_come_from_ninja_when_the_manifest_is_absent(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + bin = self.root / "bin" + bin.mkdir() + ninja = bin / "ninja" + ninja.write_text("#!/usr/bin/env python3\nimport json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") + ninja.chmod(0o755) + result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}"}) + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + +if __name__ == "__main__": + unittest.main() From 94632df42ff4aad6831d2e1c56f4e65bb9968945 Mon Sep 17 00:00:00 2001 From: adriadam10 Date: Wed, 14 Oct 2026 12:31:52 +0200 Subject: [PATCH 344/353] test(tools): resolve the temp root and give the compdb stub a Windows launcher --- tools/tests/test_check_compile.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index bb79df647e..b691d60d45 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -16,7 +16,7 @@ class CheckCompileTests(unittest.TestCase): def setUp(self): holder = tempfile.TemporaryDirectory() self.addCleanup(holder.cleanup) - self.root = Path(holder.name) + self.root = Path(holder.name).resolve() subprocess.run(["git", "init", "-q", str(self.root)], check=True) subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) @@ -185,9 +185,14 @@ class CheckCompileTests(unittest.TestCase): entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') bin = self.root / "bin" bin.mkdir() - ninja = bin / "ninja" - ninja.write_text("#!/usr/bin/env python3\nimport json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") - ninja.chmod(0o755) + stub = bin / "ninja_stub.py" + stub.write_text("import json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") + if os.name == "nt": + (bin / "ninja.cmd").write_text(f'@"{sys.executable}" "{stub}"\n', encoding="utf-8") + else: + ninja = bin / "ninja" + ninja.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{stub}"\n', encoding="utf-8") + ninja.chmod(0o755) result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}"}) self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) From 13b1113d93137cea438efaeb24ddf476531962fc Mon Sep 17 00:00:00 2001 From: Renan Pires Date: Mon, 12 Oct 2026 12:51:02 -0300 Subject: [PATCH 345/353] fix(libkernel): find main-program symbols when the executable is not position independent --- core/libs/prx/libkernel/CMakeLists.txt | 12 ++++++++ .../libkernel/Module/src/DynamicLoader.cpp | 29 ++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2369615a50..a37ee2509f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -206,6 +206,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endforeach() endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_dynamic_loader_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp) + target_include_directories(guest_dynamic_loader_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_loader_fixed_base_tests PRIVATE libkernel libc) + add_dependencies(guest_dynamic_loader_fixed_base_tests guest_module_fixture) + set_target_properties(guest_dynamic_loader_fixed_base_tests PROPERTIES ENABLE_EXPORTS ON POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_dynamic_loader_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie) + add_test(NAME guest_dynamic_loader_fixed_base COMMAND guest_dynamic_loader_fixed_base_tests $) + set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES + WORKING_DIRECTORY "$") + endif() add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp) target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_module_info_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 0c1645b880..4dac33c1dd 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -62,28 +62,23 @@ void* Symbol(Module& module, const char* name) { #endif } #ifndef _WIN32 -std::uintptr_t MainProgramBase() { - static const std::uintptr_t base = [] { - struct Scan { - std::uintptr_t value = 0; - }; - Scan scan; - ::dl_iterate_phdr( - [](dl_phdr_info* info, size_t, void* data) { - auto* found = static_cast(data); - if (found->value == 0 && info->dlpi_name != nullptr && info->dlpi_name[0] == '\0') found->value = info->dlpi_addr; - return found->value == 0 ? 0 : 1; - }, - &scan); - return scan.value; +const link_map* MainProgramMap() { + static const link_map* const map = [] { + void* handle = ::dlopen(nullptr, RTLD_LAZY); + link_map* found = nullptr; + const bool resolved = handle != nullptr && ::dlinfo(handle, RTLD_DI_LINKMAP, &found) == 0 && found != nullptr; + if (handle != nullptr) ::dlclose(handle); + if (!resolved) throw std::runtime_error("dlsym: cannot find the link map of the main program"); + return found; }(); - return base; + return map; } bool FromGuestModule(void* address) { Dl_info info{}; - if (address == nullptr || !::dladdr(address, &info) || info.dli_fname == nullptr) return false; - if (reinterpret_cast(info.dli_fbase) == MainProgramBase()) return true; + link_map* image = nullptr; + if (address == nullptr || !::dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || info.dli_fname == nullptr || image == nullptr) return false; + if (image == MainProgramMap()) return true; const std::string path(info.dli_fname); return path.ends_with(".prx"); } From 75cfcec67c5492b20c764bb3ecef98268e1f89a0 Mon Sep 17 00:00:00 2001 From: Renan Pires Date: Mon, 12 Oct 2026 12:51:02 -0300 Subject: [PATCH 346/353] feat(relinker): load Linux executables at a fixed base with --linux-fixed-base --- core/relinker/CMakeLists.txt | 2 + core/relinker/cli/include/Cli.hpp | 1 + core/relinker/cli/src/CliArgs.cpp | 27 +++- core/relinker/cli/tests/CliArgsTests.cpp | 20 +++ .../elfpatcher/general/ElfConstants.hpp | 9 ++ .../elfpatcher/linux/LinuxElfPatcher.hpp | 6 +- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 150 ++++++++++++++++-- core/relinker/main.cpp | 1 + .../include/relinker/pipeline/Conversion.hpp | 1 + .../relinker/src/pipeline/Conversion.cpp | 4 +- .../relinker/tests/ConversionTests.cpp | 19 +++ .../tests/test_linux_executable_exports.py | 13 +- .../relinker/tests/test_linux_fixed_base.py | 131 +++++++++++++++ docs/user/USAGE.md | 7 +- 14 files changed, 366 insertions(+), 25 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_fixed_base.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2a01d4bee2..0b57570996 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -437,6 +437,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) + add_test(NAME linux_fixed_base COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_fixed_base.py $) add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_program_header_order.py $) add_test(NAME linux_input_program_headers COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 0038cd80b0..52a12f2356 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -25,6 +25,7 @@ struct Args { bool windowsGui = false; std::optional windowsFixedBase; bool traceImports = false; + std::optional linuxFixedBase; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index fadcbcb364..86743356d6 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -1,19 +1,33 @@ #include #include -#include #include +#include #include #include #include #include +#include namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } +static std::uint64_t ParseLinuxFixedBase(std::string_view value) { + constexpr std::uint64_t lowest = 0x10000; + constexpr std::uint64_t highest = 0x7fffffff0000; + constexpr std::uint64_t alignment = 0x10000; + const bool hexadecimal = value.starts_with("0x") || value.starts_with("0X"); + if (hexadecimal) value.remove_prefix(2); + std::uint64_t address = 0; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10); + if (value.empty() || parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < lowest || address > highest || address % alignment != 0) + throw std::runtime_error("--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"); + return address; +} + Args ParseArgs(int argc, char* argv[]) { Args args; bool unusedFilterSpecified = false; @@ -35,6 +49,11 @@ Args ParseArgs(int argc, char* argv[]) { if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < 0x20000 || address > 0x7fffffff0000 || (address & 0xffff) != 0) throw std::runtime_error("--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); args.windowsFixedBase = address; + } else if (arg == "--linux-fixed-base") { + if (args.linuxFixedBase) throw std::runtime_error("--linux-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string_view(argv[i + 1]).empty() || std::string_view(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--linux-fixed-base requires an address"); + args.linuxFixedBase = ParseLinuxFixedBase(argv[++i]); } else if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; } else if (arg == "--trap-sdk-syscall-stubs") { @@ -130,6 +149,10 @@ Args ParseArgs(int argc, char* argv[]) { throw std::runtime_error("--trace-imports requires --windows"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); + if (args.linuxFixedBase && (args.toWindows || args.toMacos)) + throw std::runtime_error("--linux-fixed-base cannot be used with --windows or --macos"); + if (args.linuxFixedBase && args.lazyBinding) + throw std::runtime_error("--linux-fixed-base conflicts with --lazy-binding"); if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty())) throw std::runtime_error(Usage()); diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 305b66e719..6d0bd857a3 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -222,6 +222,25 @@ void windowsFixedBase() { expectThrows({"--windows", "--windows-fixed-base", "0x400000", "--windows-fixed-base", "0x800000", "input.elf", "output.exe"}, "--windows-fixed-base cannot be repeated"); } +void linuxFixedBase() { + require(!parse({"input.elf", "output.elf"}).linuxFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) + require(parse({"--linux-fixed-base", value, "input.elf", "output.elf"}).linuxFixedBase == 0x400000, "fixed base was parsed incorrectly"); + require(parse({"input.elf", "--linux-fixed-base", "0x10000", "output.elf"}).linuxFixedBase == 0x10000, "lowest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x7fffffff0000", "input.elf", "output.elf"}).linuxFixedBase == 0x7fffffff0000, "highest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x400000000", "input.elf", "output.elf"}).linuxFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + const std::string range = "--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"; + for (const auto& value : {"0", "0x0", "0xf000", "0x400001", "0x408000", "0x", "0X", "-4194304", "+4194304", " 4194304", "4194304 ", "0x400000junk", "0x-400000", "18446744073709551616", "0x800000000000", "0x7fffffff10000"}) + expectThrows({"--linux-fixed-base", value, "input.elf", "output.elf"}, range); + expectThrows({"--linux-fixed-base"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "--windows", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "0x400000", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base cannot be repeated"); + expectThrows({"--windows", "--linux-fixed-base", "0x400000", "input.elf", "output.exe"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--linux-fixed-base", "0x400000", "--macos", "input.elf", "output"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--lazy-binding", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base conflicts with --lazy-binding"); +} + } void moduleDirectories() { @@ -288,6 +307,7 @@ int main() { windowsGuiRequiresWindows(); missingPositionals(); windowsFixedBase(); + linuxFixedBase(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index e0b0ee4bab..c7c6b8c0a6 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -58,6 +58,7 @@ inline constexpr std::uint64_t kDynamicHeaderAlignment = 8; inline constexpr std::uint64_t kInterpHeaderAlignment = 1; inline constexpr std::uint16_t kSyntheticProgramHeaderCount = 5; +inline constexpr std::uint32_t PT_NULL = 0; inline constexpr std::uint32_t PT_LOAD = 1; inline constexpr std::uint32_t PT_DYNAMIC = 2; inline constexpr std::uint32_t PT_INTERP = 3; @@ -70,6 +71,7 @@ inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552; inline constexpr std::uint32_t PF_X = 0x1; inline constexpr std::uint32_t PF_W = 0x2; inline constexpr std::uint32_t PF_R = 0x4; +inline constexpr std::uint32_t ET_EXEC = 2; inline constexpr std::uint32_t ET_DYN = 3; inline constexpr std::uint32_t PT_SCE_DYNLIBDATA = 0x61000000; inline constexpr std::uint32_t PT_OS_PROCPARAM = 0x61000001; @@ -143,6 +145,10 @@ inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; +inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16; +inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; +inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; +inline constexpr std::uint32_t R_X86_64_IRELATIVE = 37; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; @@ -150,7 +156,10 @@ inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; +inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint16_t SHN_UNDEF = 0; +inline constexpr std::uint16_t SHN_LORESERVE = 0xff00; +inline constexpr std::uint16_t SHN_ABS = 0xfff1; inline constexpr std::uint32_t SHT_NULL = 0; inline constexpr std::uint32_t SHT_PROGBITS = 1; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index ae5782f330..19719a7b0f 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include namespace Elfpatcher::Linux { @@ -18,7 +20,8 @@ public: std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase = std::nullopt ); std::vector Patch( @@ -37,6 +40,7 @@ private: std::shared_ptr _programHeaderLayoutBuilder; std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; + std::optional _fixedBase; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 4fd051eba5..60d7c8b690 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -8,20 +8,124 @@ #include #include #include +#include #include namespace Elfpatcher::Linux { +namespace { + +constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; + +std::string Hex(const std::uint64_t value) { + std::ostringstream text; + text << "0x" << std::hex << value; + return text.str(); +} + +std::uint64_t Rebased(const std::uint64_t base, const std::uint64_t address, const std::string& what) { + if (base > kLinuxUserAddressEnd || address > kLinuxUserAddressEnd - base) + throw Domain::RelinkerException(what + " " + Hex(address) + " lies outside the Linux user address space at fixed base " + Hex(base)); + return base + address; +} + +void RequireWholeEntries(const std::vector& table, const std::uint64_t entrySize, const std::string& what) { + if (table.size() % entrySize != 0) + throw Domain::RelinkerException(what + " is not a whole number of entries"); +} + +void RequireOnlyDependencies(const std::vector& entries) { + RequireWholeEntries(entries, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < entries.size(); entry += kDynEntrySize) { + const auto tag = static_cast(Io::ReadU64(entries, entry)); + if (tag != DT_NEEDED) + throw Domain::RelinkerException("Dynamic tag " + Hex(static_cast(tag)) + " cannot be placed at a fixed Linux base"); + } +} + +std::vector RebasedSymbols(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kSymEntrySize, "Dynamic symbol table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kSymEntrySize) { + const auto section = Io::ReadU16(rebased, entry + 6); + if (section == SHN_UNDEF || section == SHN_ABS || (rebased[entry + 4] & 0xf) == STT_TLS) + continue; + if (section >= SHN_LORESERVE) + throw Domain::RelinkerException("Symbol section index " + Hex(section) + " cannot be placed at a fixed Linux base"); + Io::WriteU64(rebased, entry + 8, Rebased(base, Io::ReadU64(rebased, entry + 8), "Symbol")); + } + return rebased; +} + +std::vector RebasedRelocations(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kRelaEntrySize) { + const auto type = static_cast(Io::ReadU64(rebased, entry + 8)); + switch (type) { + case R_X86_64_NONE: + continue; + case R_X86_64_RELATIVE: + case R_X86_64_IRELATIVE: { + const auto addend = static_cast(Io::ReadU64(rebased, entry + 16)); + if (addend < 0) + throw Domain::RelinkerException("Relocation target " + std::to_string(addend) + " lies below the image at a fixed Linux base"); + Io::WriteU64(rebased, entry + 16, Rebased(base, static_cast(addend), "Relocation target")); + break; + } + case R_X86_64_64: + case R_X86_64_PC32: + case R_X86_64_COPY: + case R_X86_64_GLOB_DAT: + case R_X86_64_JUMP_SLOT: + case R_X86_64_32: + case R_X86_64_32S: + case R_X86_64_DTPMOD64: + case R_X86_64_DTPOFF64: + case R_X86_64_TPOFF64: + break; + default: + throw Domain::RelinkerException("Relocation type " + std::to_string(type) + " cannot be placed at a fixed Linux base"); + } + Io::WriteU64(rebased, entry, Rebased(base, Io::ReadU64(rebased, entry), "Relocation offset")); + } + return rebased; +} + +void RebaseProgramHeaders(std::vector& buf, const std::uint64_t tableOffset, const std::uint16_t entrySize, const std::uint16_t count, const std::uint64_t base) { + for (std::uint16_t index = 0; index < count; ++index) { + const auto header = static_cast(tableOffset + static_cast(index) * entrySize); + const auto type = Io::ReadU32(buf, header + kPhdrTypeOffset); + if (type == PT_NULL || type == PT_GNU_STACK) + continue; + const auto address = Rebased(base, Io::ReadU64(buf, header + kPhdrVaddrOffset), "Program header address"); + const auto physical = Rebased(base, Io::ReadU64(buf, header + kPhdrPaddrOffset), "Program header physical address"); + if (type == PT_LOAD) { + const auto alignment = Io::ReadU64(buf, header + kPhdrAlignOffset); + if (alignment > 1 && base % alignment != 0) + throw Domain::RelinkerException("Fixed Linux base " + Hex(base) + " is not a multiple of the PT_LOAD alignment " + Hex(alignment)); + if (Io::ReadU64(buf, header + kPhdrMemSizeOffset) > kLinuxUserAddressEnd - address) + throw Domain::RelinkerException("PT_LOAD at " + Hex(address) + " ends outside the Linux user address space"); + } + Io::WriteU64(buf, header + kPhdrVaddrOffset, address); + Io::WriteU64(buf, header + kPhdrPaddrOffset, physical); + } +} + +} + LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) + , _fixedBase(fixedBase) { } @@ -78,13 +182,26 @@ std::vector LinuxElfPatcher::Patch( { if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); + if (_fixedBase && lazyBinding) + throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_fixedBase) + RequireOnlyDependencies(dynSection.DynamicSegmentData); + const std::uint64_t base = _fixedBase.value_or(0); + const auto address = [this, base](const std::uint64_t vaddr) { + return _fixedBase ? Rebased(base, vaddr, "Address") : vaddr; + }; + const auto table = [this, base](const std::vector& data, const bool relocations) { + if (!_fixedBase) return data; + return relocations ? RebasedRelocations(data, base) : RebasedSymbols(data, base); + }; std::vector buf = sourceElf; + const std::uint32_t type = _fixedBase ? ET_EXEC : ET_DYN; buf[kEhdrOsAbiOffset] = 0; buf[kEhdrAbiVersionOffset] = 0; - buf[kEhdrTypeOffset] = static_cast(ET_DYN & 0xFF); - buf[kEhdrTypeOffset + 1] = static_cast((ET_DYN >> 8) & 0xFF); + buf[kEhdrTypeOffset] = static_cast(type & 0xFF); + buf[kEhdrTypeOffset + 1] = static_cast((type >> 8) & 0xFF); _byteWriter->WriteU64(buf, kEhdrShOffOffset, 0); _byteWriter->WriteU16(buf, kEhdrShNumOffset, 0); _byteWriter->WriteU16(buf, kEhdrShStrNdxOffset, 0); @@ -109,7 +226,8 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.DynSymData) + const auto symbols = table(dynSection.DynSymData, false); + for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); @@ -123,12 +241,14 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaData) + const auto relocations = table(dynSection.RelaData, true); + for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); const auto relaPltOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaPltData) + const auto pltRelocations = table(dynSection.RelaPltData, true); + for (std::uint8_t b : pltRelocations) buf.push_back(b); alignBuf(buf, kRelaPltAlignment); @@ -144,22 +264,22 @@ std::vector LinuxElfPatcher::Patch( std::vector dynSegBuf; for (std::uint8_t b : dynSection.DynamicSegmentData) dynSegBuf.push_back(b); - _appendDynEntry(dynSegBuf, DT_STRTAB, vaddrOfExtraBlockOffset(dynStrOff)); + _appendDynEntry(dynSegBuf, DT_STRTAB, address(vaddrOfExtraBlockOffset(dynStrOff))); _appendDynEntry(dynSegBuf, DT_STRSZ, dynStrSize); - _appendDynEntry(dynSegBuf, DT_SYMTAB, vaddrOfExtraBlockOffset(dynSymOff)); + _appendDynEntry(dynSegBuf, DT_SYMTAB, address(vaddrOfExtraBlockOffset(dynSymOff))); _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); - _appendDynEntry(dynSegBuf, DT_HASH, vaddrOfExtraBlockOffset(hashOff)); + _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); if (!dynSection.RelaData.empty()) { - _appendDynEntry(dynSegBuf, DT_RELA, vaddrOfExtraBlockOffset(relaOff)); + _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { - _appendDynEntry(dynSegBuf, DT_JMPREL, vaddrOfExtraBlockOffset(relaPltOff)); + _appendDynEntry(dynSegBuf, DT_JMPREL, address(vaddrOfExtraBlockOffset(relaPltOff))); _appendDynEntry(dynSegBuf, DT_PLTRELSZ, dynSection.RelaPltData.size()); _appendDynEntry(dynSegBuf, DT_PLTREL, static_cast(DT_RELA)); - _appendDynEntry(dynSegBuf, DT_PLTGOT, originalPltGotVaddr); + _appendDynEntry(dynSegBuf, DT_PLTGOT, address(originalPltGotVaddr)); } if (!lazyBinding) _appendDynEntry(dynSegBuf, DT_FLAGS, DF_BIND_NOW); @@ -173,10 +293,10 @@ std::vector LinuxElfPatcher::Patch( const std::uint64_t realEntryVaddr = *reinterpret_cast(buf.data() + kEhdrEntryOffset); const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); - const auto stubBytes = _entryStubBuilder->BuildEntryStub(stubVaddr, realEntryVaddr); + const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); for (std::uint8_t b : stubBytes) buf.push_back(b); - _byteWriter->WriteU64(buf, kEhdrEntryOffset, stubVaddr); + _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) _appendTrampoline(buf, site, extraBlockOff, vaddrOfExtraBlockOffset); @@ -214,6 +334,8 @@ std::vector LinuxElfPatcher::Patch( const std::uint16_t writtenPh = _programHeaderLayoutBuilder->WriteLayout(buf, layoutRequest); _byteWriter->WriteU16(buf, kEhdrPhNumOffset, writtenPh); + if (_fixedBase) + RebaseProgramHeaders(buf, phOff, phEntSize, writtenPh, base); SectionHeaderTableRequest sectionRequest{}; sectionRequest.DynStrOffset = dynStrOff; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index cf63c3296e..557840d16a 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -60,6 +60,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.TraceImports = args.traceImports; options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; + options.LinuxFixedBase = args.linuxFixedBase; options.ExcludedModules = args.excludedSceModules; progress.IncludeFile(Io::Utf8Path(input.InputPath)); progress.IncludeFile(Io::Utf8Path(input.OutputPath)); diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index d6b82a2261..7feb6e2939 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -21,6 +21,7 @@ struct ConversionOptions { bool TraceImports = false; std::optional WindowsFixedBase; bool WindowsDiagnostics = false; + std::optional LinuxFixedBase; std::string RunPath = "$ORIGIN/libs"; std::set ExcludedModules; }; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 798d086efb..ccbc0bd450 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -44,6 +44,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (input.InputPath.lexically_normal() == input.OutputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite the input executable"); if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); if (options.WindowsDiagnostics && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Windows diagnostics require the Windows target"); + if (options.LinuxFixedBase && options.Target != TargetSystem::Linux) throw Domain::RelinkerException("A fixed Linux base requires the Linux target"); const bool windows = options.Target == TargetSystem::Windows; const bool macos = options.Target == TargetSystem::MacOs; inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 0, 1}); @@ -111,7 +112,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti byteWriter ), std::make_shared(byteWriter), - byteWriter + byteWriter, + options.LinuxFixedBase ); } diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 9e58d06fc7..5a5e0b20e3 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -119,6 +119,24 @@ void convert() { require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); } +void fixedBase() { + TestProgress progress; + const Relinker::Conversion converter(progress); + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto plain = converter.Convert(source, {}); + Relinker::ConversionOptions options; + options.LinuxFixedBase = 0x400000000; + const auto fixed = converter.Convert(source, options); + require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + options.Target = target; + reject([&] { converter.Convert(source, options); }); + } +} + void scanning() { TestProgress progress; const auto scanner = Relinker::MakeSyscallScanner(progress); @@ -219,6 +237,7 @@ void cancellation() { int main() { try { convert(); + fixedBase(); scanning(); diagnosticContext(); cancellation(); diff --git a/core/relinker/relinker/tests/test_linux_executable_exports.py b/core/relinker/relinker/tests/test_linux_executable_exports.py index fc878aefcd..bc27b67838 100644 --- a/core/relinker/relinker/tests/test_linux_executable_exports.py +++ b/core/relinker/relinker/tests/test_linux_executable_exports.py @@ -36,12 +36,13 @@ def main(): struct.pack_into("` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | | `--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: 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`. | | `unused-filter=0` | Keep all imported NID references. | @@ -79,7 +80,11 @@ The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, ## Runtime layout -For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change Linux or macOS output. +For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. + +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux. Use this option only when the executable requires a fixed address. Without it, Linux output is a position-independent executable as before. + +The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. Paths are relative to the output executable: From 3d101e8bc54c4eb34681172b14b40b3a999e3569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 12:36:59 +0200 Subject: [PATCH 347/353] fix(relinker): drop constants main already defines after the rebase --- .../elfpatcher/include/elfpatcher/general/ElfConstants.hpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index c7c6b8c0a6..95099d7477 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -145,9 +145,6 @@ inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; -inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16; -inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; -inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_IRELATIVE = 37; inline constexpr std::uint8_t STB_GLOBAL = 1; @@ -156,7 +153,6 @@ inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; -inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint16_t SHN_UNDEF = 0; inline constexpr std::uint16_t SHN_LORESERVE = 0xff00; inline constexpr std::uint16_t SHN_ABS = 0xfff1; From 00fe89c536bd5731e327802935e1740c3291f92c Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Mon, 12 Oct 2026 18:30:16 -0300 Subject: [PATCH 348/353] perf(agc): reserve resource keys for their serialized payload --- .../Graphics/src/ShaderResources.cpp | 14 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 78 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f09d2cbcb6..4926b3dd01 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" @@ -1830,7 +1831,18 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB std::vector ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) { Require(shader.program != nullptr, "missing compiled shader"); std::vector key; - key.reserve(8u + shader.program->bindings.size() * 12u); + std::size_t words = 5; + const auto addWords = [&](std::size_t count) { + if (count > key.max_size() - words) throw std::length_error("resource content key exceeds vector limits"); + words += count; + }; + for (const auto& binding : shader.program->bindings) { + addWords(18); + addWords(binding.imageSamplers.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)); + } + key.reserve(words); VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); return key; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1eab546a6d..b597cd1835 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -5942,6 +5943,78 @@ void parallelCompareTests() { CompareSpansFrom({}, none, 0); } +void ResourceContentKeyTests(bool benchmark) { + using AgcDriver::Graphics::ShaderResources; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + for (const auto count : {0u, 1u, 31u, 32u, 33u, 16384u}) { + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::GuestImages; + images.count = count; + images.guestDescriptor.resize(static_cast(count) * 8u); + for (std::size_t i = 0; i < images.guestDescriptor.size(); ++i) images.guestDescriptor[i] = static_cast(i * 1664525u + 1013904223u); + for (auto* bits : {&images.imageWritten, &images.samplerDepthCompare, &images.imageDepthCompare, &images.imageAtomic, &images.samplerUnnormalized, &images.samplerIntegerBorder, &images.imageUnnormalized, &images.imageUnnormalizedBaseLevel, &images.bufferWritten}) { + bits->resize(count); + for (std::size_t i = 0; i < bits->size(); ++i) (*bits)[i] = i % 3u == 0; + } + images.imageSamplers.resize(count, 7u); + program.bindings.push_back(std::move(images)); + ShaderRecompiler::DescriptorBinding data{}; + data.kind = DescriptorKind::StorageBuffer; + data.role = DescriptorRole::ShaderData; + data.count = 1; + data.guestDescriptor.resize(257u, 0x12345678u); + program.bindings.push_back(std::move(data)); + ShaderRecompiler::DescriptorBinding buffers{}; + buffers.kind = DescriptorKind::StorageBuffer; + buffers.role = DescriptorRole::GuestBuffers; + buffers.count = 1; + buffers.guestDescriptor = {0x1000u, 0x10000u, 4u, 0x1234u}; + buffers.bufferWritten = {false}; + program.bindings.push_back(std::move(buffers)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + for (const bool dataWords : {false, true}) for (const bool movable : {false, true}) { + const auto original = ShaderResources::ContentKey(shader, dataWords, movable); + program.bindings[1].guestDescriptor.back() ^= 1u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == (dataWords && !movable), "content key changed its data-word policy"); + program.bindings[1].guestDescriptor.back() ^= 1u; + program.bindings[2].guestDescriptor[0] += 0x1000u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == !movable, "content key changed its movable-buffer policy"); + program.bindings[2].guestDescriptor[0] -= 0x1000u; + program.bindings[2].bufferWritten[0] = true; + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the written-buffer mask"); + program.bindings[2].bufferWritten[0] = false; + if (count != 0) { + for (auto* bits : {&program.bindings[0].imageWritten, &program.bindings[0].samplerDepthCompare, &program.bindings[0].imageDepthCompare, &program.bindings[0].imageAtomic, &program.bindings[0].samplerUnnormalized, &program.bindings[0].samplerIntegerBorder, &program.bindings[0].imageUnnormalized, &program.bindings[0].imageUnnormalizedBaseLevel}) { + bits->back().flip(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last packed flag"); + bits->back().flip(); + } + ++program.bindings[0].imageSamplers.back(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association"); + --program.bindings[0].imageSamplers.back(); + } + std::uint64_t hash = 14695981039346656037ull; + for (const auto word : original) hash = (hash ^ word) * 1099511628211ull; + std::cout << "content-key count=" << count << " data=" << dataWords << " movable=" << movable << " words=" << original.size() << " capacity=" << original.capacity() << " hash=" << hash; + if (benchmark) { + constexpr std::size_t repeats = 256; + std::uint64_t checksum = 0; + const auto begin = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < repeats; ++i) { + const auto key = ShaderResources::ContentKey(shader, dataWords, movable); + checksum += key.size() + key.back(); + } + const auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); + std::cout << " us/key=" << elapsed / repeats << " checksum=" << checksum; + } + std::cout << '\n'; + } + } +} + int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "200"); @@ -5988,6 +6061,10 @@ int main(int argc, char** argv) { std::cout << "Existing undithered alpha-to-coverage states passed\n"; return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--resource-content-key" || std::string_view(argv[1]) == "--benchmark-resource-content-key")) { + ResourceContentKeyTests(std::string_view(argv[1]) == "--benchmark-resource-content-key"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; @@ -6040,6 +6117,7 @@ int main(int argc, char** argv) { AgcDriver::Graphics::ValidateDepthBounds(unrestricted, bounded); } RunGuestLeaseWaitTests(); + ResourceContentKeyTests(false); stateTests(); pipelineLibrarySetTests(); hdrComponentSwapTests(); From 6d7b1d11bb08262a38b3af0b8bda2e8c1d8b02e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 12:45:00 +0200 Subject: [PATCH 349/353] test(agc): pin that the content key reserves exactly its serialized word count --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b597cd1835..89a5761f97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5996,6 +5996,7 @@ void ResourceContentKeyTests(bool benchmark) { Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association"); --program.bindings[0].imageSamplers.back(); } + Require(original.capacity() == original.size(), "content key reserved a different word count than it serialized"); std::uint64_t hash = 14695981039346656037ull; for (const auto word : original) hash = (hash ^ word) * 1099511628211ull; std::cout << "content-key count=" << count << " data=" << dataWords << " movable=" << movable << " words=" << original.size() << " capacity=" << original.capacity() << " hash=" << hash; From fe4831f350035ae609097248a2cee93197ca0e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 13:13:22 +0200 Subject: [PATCH 350/353] ci(build): check out nested submodules 3rdparty/mbedtls (#3897) has its own framework submodule. The PR build fetched it in its merge step, but push builds on main skipped that step and failed at configure. Co-Authored-By: Claude --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eca6c31edd..c107c34020 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -227,7 +227,7 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true + submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -297,7 +297,7 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true + submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash From 15503d11fed6f8d62c39e1f6df3796afe05578ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 13:41:55 +0200 Subject: [PATCH 351/353] ci(build): build several pull requests together Co-Authored-By: Claude --- .github/workflows/build.yml | 108 +++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c107c34020..85af5ffd2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: pr: - description: Pull request number to build + description: Pull request numbers to build together, separated by spaces or commas (up to 8) required: true type: string linux: @@ -77,38 +77,47 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) { - throw new Error('Enter a positive pull request number'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas'); } } - const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number; - if (!Number.isSafeInteger(pull_number) || pull_number < 1) { + const numbers = manual + ? [...new Set(process.env.PR_NUMBER.trim().split(/[ ,]+/).map(Number))] + : [context.payload.pull_request.number]; + if (numbers.some((pull_number) => !Number.isSafeInteger(pull_number) || pull_number < 1)) { throw new Error('Invalid pull request number'); } - let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); - for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); - } - if (pr.state !== 'open' || pr.base.ref !== 'main') { - throw new Error('The pull request must be open and target main'); - } - if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { - throw new Error('The pull request has newer commits; use the latest run'); - } - if (pr.mergeable === false) { - throw new Error('The pull request does not merge cleanly into main; rebase it first'); - } + const pulls = []; + const paths = []; + let truncated = false; let relinker = manual; - if (!manual) { - const { data: commits } = await github.rest.repos.listCommits({ - owner, repo, sha: 'main', author: pr.user.login, per_page: 1 - }); - relinker = commits.length > 0; + for (const pull_number of numbers) { + let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); + for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); + } + if (pr.state !== 'open' || pr.base.ref !== 'main') { + throw new Error(`Pull request #${pull_number} must be open and target main`); + } + if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { + throw new Error('The pull request has newer commits; use the latest run'); + } + if (pr.mergeable === false) { + throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); + } + if (!manual) { + const { data: commits } = await github.rest.repos.listCommits({ + owner, repo, sha: 'main', author: pr.user.login, per_page: 1 + }); + relinker = commits.length > 0; + } + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); + truncated ||= files.length >= 3000; + paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + pulls.push({ number: pull_number, head: pr.head.sha }); } - const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); - const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); - const touches = (...prefixes) => files.length >= 3000 || + const touches = (...prefixes) => truncated || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; @@ -121,15 +130,16 @@ jobs: const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); - core.setOutput('pr', String(pull_number)); - core.setOutput('head', pr.head.sha); + core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); + core.setOutput('head', pulls.map((pull) => pull.head).join(' ')); core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); - core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); + const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); + merged.forEach((line) => core.info(line)); await core.summary.addRaw( - `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + + `${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); @@ -167,9 +177,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - if: runner.os == 'Windows' id: toolchain uses: actions/cache@v6 @@ -235,9 +251,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - uses: hendrikmuhs/ccache-action@v1.2.24 with: @@ -305,9 +327,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - id: toolchain uses: actions/cache@v6 From dca6020daaef0a161b849ca9a7ef5e8721498d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 13:48:47 +0200 Subject: [PATCH 352/353] test(agc): word the lavapipe skips of two tests as device limitations Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- .../prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c740eb7275..65952bbeb7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6734,7 +6734,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { if (!shadowedKeyStoreTests(device, recorder)) { - std::cout << "skipped, unit shadows over a watched host import are unavailable\n"; + std::cout << "skipped, the device imports no watched host memory under a unit shadow\n"; return 77; } std::cout << "DCC keys under unit shadows tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index dea1ea5f19..81807e14b2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -458,7 +458,7 @@ int main(int argc, char** argv) { DitheredCoverage(*device); } catch (const std::runtime_error& error) { if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; - std::printf("skipped, eight-sample color attachments are unavailable: %s\n", error.what()); + std::printf("skipped, the device lacks eight-sample color attachments: %s\n", error.what()); return VulkanTestSkipped; } std::puts("dithered eight-sample alpha-to-coverage tests passed"); From e53b57478248908bd37971158a97dd42c156fc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 13:50:34 +0200 Subject: [PATCH 353/353] ci(build): skip the pull request build of a pull request already closed Co-Authored-By: Claude --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85af5ffd2a..a851258417 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,10 @@ jobs: await new Promise((resolve) => setTimeout(resolve, 3000)); ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); } + if (!manual && pr.state !== 'open') { + await core.summary.addRaw(`PR #${pull_number} was ${pr.merged ? 'merged' : 'closed'} before this run started: nothing to build`).write(); + return; + } if (pr.state !== 'open' || pr.base.ref !== 'main') { throw new Error(`Pull request #${pull_number} must be open and target main`); }