From 480b04801c8c68bae377933b77d2e8e461a4d478 Mon Sep 17 00:00:00 2001 From: krishnaraya Date: Wed, 7 Oct 2026 15:46:35 -0400 Subject: [PATCH 001/277] fix(shader): compute V_FRACT_F32 from the exact fraction of |x| Co-authored-by: Hashaam Zafar <68960089+hashaam1217@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) --- .../SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dd8c915aa..63b68a43d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -995,7 +995,13 @@ std::uint32_t EmitFPTrunc32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitFPFract32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitGlsl(state, arg0); + const auto magnitude = EmitGlsl(state, arg0); + const auto fraction = EmitGlsl(state, magnitude); + const auto complement = EmitFPSub32(state, ConstantF32(state, 0x3f800000u), fraction); + const auto negative = Binary(state, spv::OpFOrdLessThan, TypeBool(state), arg0, ConstantF32(state, 0u)); + const auto partial = Binary(state, spv::OpFOrdNotEqual, TypeBool(state), fraction, ConstantF32(state, 0u)); + const auto below = Binary(state, spv::OpLogicalAnd, TypeBool(state), negative, partial); + return Select(state, TypeF32(state), below, complement, fraction); } std::uint32_t EmitBitCastF16U16(SpirvEmitterState& state, std::uint32_t arg0) { From 97dfbfe4e0ec4c351c7dfb5f36fa05e724488169 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:19:33 -0700 Subject: [PATCH 002/277] test(videoout): make SDL video driver configurable --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index cf2807919..4df04c344 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -32,6 +32,9 @@ target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDrive set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING + "SDL video driver for VideoOut flip tests") + set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flipMode IN ITEMS lifetime reopen decode controls overlay) if(flipMode STREQUAL "lifetime") @@ -39,7 +42,7 @@ if(BUILD_TESTING) else() add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) endif() - set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=offscreen") + set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") if(WIN32) set_tests_properties(video_out_flip_${flipMode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From 54c839959fd0b0de2923a048c7bc3a23bddd9599 Mon Sep 17 00:00:00 2001 From: goncalompontes Date: Sat, 10 Oct 2026 21:07:55 +0100 Subject: [PATCH 003/277] feat(shader): decode GFX10 VOP3-encoded interpolation v_interp_p1_f32_e64, v_interp_p2_f32_e64 and v_interp_mov_f32_e64 (VOP3Interp opcodes 0x200, 0x201 and 0x202) reach the decoder as VOP3 and were refused as unimplemented VOP3 opcodes. Decode them into the existing VInterpP1F32, VInterpP2F32 and VInterpMovF32 opcodes with the VOP3Interp operand layout, so the interpolation translation already used by the VINTRP encodings handles them. Encodings and operand layout follow LLVM's gfx10 MC tests for VOP3Interp_Real_gfx10 (D69348). The interpolation parameter is the low six bits of src0, the channel bits 6:7, the coordinate (or the interpolation parameter of the mov form) is src1, and clamp and omod reach the destination. op_sel, the packed-half selector, a third source and abs/neg on operands that do not exist throw. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +++ .../tests/Vop3InterpDecode.cpp | 100 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 44 ++++++++ 3 files changed, 157 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d24c90ab0..a8a566cab 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -470,6 +470,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_vop3_interp_decode_tests EXCLUDE_FROM_ALL tests/Vop3InterpDecode.cpp) +target_link_libraries(agc_vop3_interp_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vop3_interp_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vop3_interp_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_vop3_interp_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vop3_interp_decode COMMAND agc_vop3_interp_decode_tests) + if(WIN32) + set_tests_properties(agc_vop3_interp_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp) target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) target_link_libraries(agc_pass_hazards_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp new file mode 100644 index 000000000..a8c267b53 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -0,0 +1,100 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Accepted { + std::vector words; + RdnaOpcode op; + const char* name; + RdnaOperandKind source0Kind; + std::uint32_t source0Value; + std::uint32_t attribute; + std::uint32_t channel; + bool absolute; + bool negate; + bool clamp; + std::uint32_t omod; +}; + +struct Refused { + std::vector words; + const char* reason; + const char* name; +}; + +} + +int main() { + constexpr std::uint32_t Destination = 5; + const std::vector accepted{ + {{0xd6000005u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u}, + {{0xd6010005u, 0x00020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u}, + {{0xd6020005u, 0x00000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0002041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x00020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0003fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u}, + {{0xd6000205u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u}, + {{0xd6000005u, 0x40020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, + {{0xd6008005u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, + {{0xd6000005u, 0x08020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, + }; + const std::vector refused{ + {{0xd6000005u, 0x00000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, + {{0xd6020005u, 0x00000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"}, + {{0xd6000805u, 0x00020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"}, + {{0xd6000005u, 0x00020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, + {{0xd6000005u, 0x00060400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with a third source"}, + {{0xd6000105u, 0x00020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, + {{0xd6020005u, 0x20000000u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + }; + int failures = 0; + for (const auto& test : accepted) { + auto code = test.words; + code.push_back(0xbf810000u); + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (decoded.instructions.empty()) { + std::fprintf(stderr, "%s decoded no instruction\n", test.name); + ++failures; + continue; + } + const auto& instruction = decoded.instructions[0]; + const auto source0Value = test.source0Kind == RdnaOperandKind::VectorRegister ? instruction.source0.reg : instruction.source0.value; + if (instruction.op != test.op || instruction.wordCount != test.words.size() || instruction.family != RdnaInstructionFamily::VOP3 || + instruction.destination.kind != RdnaOperandKind::VectorRegister || instruction.destination.reg != Destination || + instruction.destination.clamp != test.clamp || instruction.destination.omod != test.omod || instruction.sourceCount != 3u || + instruction.source0.kind != test.source0Kind || source0Value != test.source0Value || + instruction.source0.absolute != test.absolute || instruction.source0.negate != test.negate || + instruction.source1.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source1.value != test.attribute || + instruction.source2.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source2.value != test.channel) { + std::fprintf(stderr, "%s decoded to the wrong opcode or operands\n", test.name); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "%s was refused: %s\n", test.name, error.what()); + ++failures; + } + } + for (const auto& test : refused) { + auto code = test.words; + code.push_back(0xbf810000u); + std::string refusal; + try { + static_cast(RdnaInstructionDecoder{}.Decode(code)); + } catch (const std::exception& error) { + refusal = error.what(); + } + if (refusal.find(test.reason) == std::string::npos) { + std::fprintf(stderr, "%s was not refused\n", test.name); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 521e629c2..00b7078af 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -529,6 +529,9 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x311u, RdnaOpcode::VPackB32F16}, {0x314u, RdnaOpcode::VLshlrevB16}, {0x319u, RdnaOpcode::VSubrevI32}, + {0x200u, RdnaOpcode::VInterpP1F32}, + {0x201u, RdnaOpcode::VInterpP2F32}, + {0x202u, RdnaOpcode::VInterpMovF32}, {0x342u, RdnaOpcode::VInterpP1llF16}, {0x343u, RdnaOpcode::VInterpP1lvF16}, {0x344u, RdnaOpcode::VPermB32}, @@ -2114,6 +2117,43 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst } } +bool isVop3F32InterpolationOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VInterpP1F32 || opcode == RdnaOpcode::VInterpP2F32 || opcode == RdnaOpcode::VInterpMovF32; +} + +void decodeVop3F32Interpolation(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 moves = instruction.op == RdnaOpcode::VInterpMovF32; + if ((abs & 0x5u) != 0u || (neg & 0x5u) != 0u || opSel != 0u || src2 != 0u || (src0 >> 8u) != 0u || (moves && (abs | neg) != 0u)) { + throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); + } + if (moves) { + if (src1 >= 3u) { + throw std::invalid_argument("VOP3 interpolation source is not an interpolation parameter"); + } + } else if (src1 < 256u) { + throw std::invalid_argument("VOP3 interpolation source is not a vector register"); + } + instruction.destination = DecodeRdnaVectorGpr(vdst); + instruction.destination.clamp = clamp != 0u; + instruction.destination.omod = omod; + if (moves) { + instruction.source0.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source0.value = src1; + instruction.source0.signedVal = static_cast(src1); + } else { + instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); + instruction.source0.absolute = (abs & 0x2u) != 0u; + instruction.source0.negate = (neg & 0x2u) != 0u; + } + instruction.source1.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source1.value = src0 & 0x3fu; + instruction.source1.signedVal = static_cast(instruction.source1.value); + instruction.source2.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source2.value = (src0 >> 6u) & 0x3u; + instruction.source2.signedVal = static_cast(instruction.source2.value); + instruction.sourceCount = 3; +} + void applyNativeVop3SourceModifiers(RdnaInstruction& instruction, std::uint32_t abs, std::uint32_t neg) { RdnaOperand* sources[] = {&instruction.source0, &instruction.source1, &instruction.source2}; for (std::uint32_t i = 0; i < instruction.sourceCount && i < 3u; ++i) { @@ -2278,6 +2318,10 @@ RdnaInstruction DecodeRdnaVop3(std::uint32_t programCounter, std::span Date: Sat, 10 Oct 2026 20:42:04 -0400 Subject: [PATCH 004/277] perf(shader): pick f16 min, max and med3 results on the bits v_min_f16, v_max_f16, v_min3_f16, v_max3_f16, v_med3_f16, v_pk_min_f16 and v_pk_max_f16 converted their sources to f32, ran the f32 min, max or med3 with its NaN and signed-zero rules, and converted the result back to f16 before applying the f16 NaN rules on the bits. The result is always one of the sources, or for two zeros their sign bits combined, so it is now chosen on the f16 bits: an integer comparison of the bits with the sign folded into an ordering key, the same AND/OR rule for two zeros, then the existing NaN, IEEE_MODE, denormal flush and clamp rules. The integer comparison doesn't depend on how the driver converts f16 denormals to f32. Comparing the f32 conversions instead fails on an Intel UHD, whose UnpackHalf2x16 makes f16 denormals zero when the module declares no f16 DenormPreserve, as these instructions no longer convert to f16. New test agc_driver_float16_min_max_bits checks the seven instructions (VOP2 and VOP3 forms with abs, neg, op_sel and clamp; packed op_sel, op_sel_hi and neg) on random and special inputs against a model of those rules, in seven float modes including the f16 input denormal flush. Current main gives the same results as the model on an RTX 4060, an Intel UHD and lavapipe. --- .../tests/execution/Float16MinMaxBits.cpp | 305 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/FloatInstructions.cpp | 72 +++-- 3 files changed, 344 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp new file mode 100644 index 000000000..8c6b30f64 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -0,0 +1,305 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Stride = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ScalarCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x74140b04, + 0x72160b04, + 0xd539810c, 0x20020d04, + 0xd53a800d, 0x40020d05, + 0xd751000e, 0x041a0b04, + 0xd754280f, 0x441a0b04, + 0xd7570010, 0x041a0b04, + 0xd7579111, 0x841e0b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc11400a, 0x18020b04, + 0xcc12400b, 0x18020b04, + 0xcc12ca0c, 0x30020d04, + 0xcc11c00d, 0x58020f05, + 0xe0781000, 0x80010a03, 0xbf810000, +}; + +enum class Kind { Min, Max, Min3, Max3, Med3 }; + +struct Source { + std::uint32_t reg; + bool opSel = false; + bool opSelHi = true; + bool absolute = false; + bool negate = false; + bool negateHi = false; +}; + +struct Check { + const char* name; + Kind kind; + std::array sources; + bool clamp = false; +}; + +const std::array ScalarChecks{{ + {"v_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v12, -|v4|, v6 clamp", Kind::Max, {{{0u, false, true, true, true}, {2u}, {}}}, true}, + {"v_min_f16 v13, v5, -v6 clamp", Kind::Min, {{{1u}, {2u, false, true, false, true}, {}}}, true}, + {"v_min3_f16 v14, v4, v5, v6", Kind::Min3, {{{0u}, {1u}, {2u}}}}, + {"v_max3_f16 v15, v4, -v5, v6 op_sel:[1,0,1,0]", Kind::Max3, {{{0u, true}, {1u, false, true, false, true}, {2u, true}}}}, + {"v_med3_f16 v16, v4, v5, v6", Kind::Med3, {{{0u}, {1u}, {2u}}}}, + {"v_med3_f16 v17, |v4|, v5, -v7 op_sel:[0,1,0,0] clamp", Kind::Med3, {{{0u, false, true, true}, {1u, true}, {3u, false, true, false, true}}}, true}, +}}; + +const std::array PackedChecks{{ + {"v_pk_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v12, v4, v6 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1] clamp", Kind::Max, {{{0u, true, false, false, true, false}, {2u, false, true, false, false, true}, {}}}, true}, + {"v_pk_min_f16 v13, v5, v7 neg_lo:[0,1] clamp", Kind::Min, {{{1u}, {3u, false, true, false, true}, {}}}, true}, +}}; + +struct Mode { + const char* name; + std::optional floatMode; + bool packed = true; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, + {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false}, +}}; + +std::uint64_t Seed = 0x2545f4914f6cdd1dull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 9u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | 0x3c00u | (bits & 0x3u); + default: return bits & 0xffffu; + } +} + +bool IsNan(std::uint32_t bits) { return (bits & 0x7fffu) > 0x7c00u; } + +bool IsSnan(std::uint32_t bits) { return IsNan(bits) && (bits & 0x0200u) == 0u; } + +double Value(std::uint32_t bits) { + const double sign = (bits & 0x8000u) != 0u ? -1.0 : 1.0; + const std::uint32_t exponent = (bits >> 10u) & 0x1fu; + const std::uint32_t mantissa = bits & 0x3ffu; + if (exponent == 0u) return sign * std::ldexp(static_cast(mantissa), -24); + if (exponent == 31u) return mantissa != 0u ? std::nan("") : sign * INFINITY; + return sign * std::ldexp(static_cast(mantissa | 0x400u), static_cast(exponent) - 25); +} + +struct Model { + bool ieee; + bool dx10; + bool flushInputs; + + struct Half { + double value; + std::uint32_t bits; + }; + + std::uint32_t Quiet(std::uint32_t bits) const { return ieee ? bits | 0x0200u : bits; } + + Half Numeric(bool maxValue, const Half& lhs, const Half& rhs) const { + const bool ordered = maxValue ? lhs.value >= rhs.value : lhs.value < rhs.value; + const bool bothZero = ((lhs.bits | rhs.bits) & 0x7fffu) == 0u; + const std::uint32_t zero = maxValue ? lhs.bits & rhs.bits : lhs.bits | rhs.bits; + return {ordered ? lhs.value : rhs.value, bothZero ? zero : (ordered ? lhs.bits : rhs.bits)}; + } + + Half Pair(bool maxValue, const Half& lhs, const Half& rhs) const { + const Half numeric = Numeric(maxValue, lhs, rhs); + std::uint32_t bits = IsNan(lhs.bits) ? rhs.bits : numeric.bits; + bits = IsNan(rhs.bits) ? lhs.bits : bits; + if (ieee) { + bits = IsSnan(rhs.bits) ? Quiet(rhs.bits) : bits; + bits = IsSnan(lhs.bits) ? Quiet(lhs.bits) : bits; + } + return {IsNan(rhs.bits) ? lhs.value : numeric.value, bits}; + } + + std::uint32_t Clamp(bool clamp, std::uint32_t bits) const { + if (!clamp) return bits; + const std::uint32_t magnitude = bits & 0x7fffu; + const bool zero = bits > 0x7fffu || magnitude > 0x7c00u; + const std::uint32_t clamped = zero ? 0u : (magnitude > 0x3c00u ? 0x3c00u : bits); + return dx10 || magnitude <= 0x7c00u ? clamped : bits; + } + + Half Read(std::uint32_t bits) const { + if (flushInputs && (bits & 0x7fffu) < 0x0400u) bits &= 0x8000u; + return {Value(bits), bits}; + } + + std::uint32_t Scalar(const Check& check, const std::uint32_t* registers) const { + std::array args{}; + for (std::uint32_t index = 0; index < 3u; ++index) { + const Source& source = check.sources[index]; + std::uint32_t bits = (registers[source.reg] >> (source.opSel ? 16u : 0u)) & 0xffffu; + if (source.absolute) bits &= 0x7fffu; + if (source.negate) bits ^= 0x8000u; + args[index] = Read(bits); + } + std::uint32_t result = 0u; + switch (check.kind) { + case Kind::Min: result = Pair(false, args[0], args[1]).bits; break; + case Kind::Max: result = Pair(true, args[0], args[1]).bits; break; + case Kind::Min3: result = Pair(false, Pair(false, args[0], args[1]), args[2]).bits; break; + case Kind::Max3: result = Pair(true, Pair(true, args[0], args[1]), args[2]).bits; break; + case Kind::Med3: { + const bool anyNan = IsNan(args[0].bits) || IsNan(args[1].bits) || IsNan(args[2].bits); + const Half minimum = Pair(false, Pair(false, args[0], args[1]), args[2]); + const Half median = Numeric(true, Numeric(false, args[0], args[1]), Numeric(false, Numeric(true, args[0], args[1]), args[2])); + result = anyNan ? minimum.bits : median.bits; + break; + } + } + return Clamp(check.clamp, result); + } + + std::uint32_t Packed(const Check& check, const std::uint32_t* registers) const { + std::uint32_t result = 0u; + for (std::uint32_t high = 0; high < 2u; ++high) { + std::array bits{}; + for (std::uint32_t index = 0; index < 2u; ++index) { + const Source& source = check.sources[index]; + const bool selectHigh = high != 0u ? source.opSelHi : source.opSel; + bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; + if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; + } + std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits; + lane = IsNan(bits[0]) ? bits[1] : lane; + lane = IsNan(bits[1]) ? bits[0] : lane; + if (ieee) { + lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane; + lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane; + } + result |= Clamp(check.clamp, lane) << (16u * high); + } + return result; + } +}; + +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; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](std::span code, const Mode& mode) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + std::uint64_t compared = 0u; + for (const auto& mode : Modes) { + const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u; + const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs}; + const auto scalar = compile(ScalarCode, mode); + std::optional packed; + if (mode.packed) packed = compile(PackedCode, mode); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t* row = &Input[tid * Inputs]; + for (std::uint32_t index = 0; index < Inputs; ++index) row[index] = Half() | (Half() << 16u); + if (tid % 8u == 0u) row[1] = row[0]; + if (tid % 8u == 1u) row[1] = row[0] ^ 0x80008000u; + if (tid % 8u == 2u) row[2] = row[1]; + if (tid % 8u == 3u) row[2] = row[0] ^ 0x80008000u; + } + const auto run = [&](const ShaderRecompiler::RecompileResult& result, std::span code, std::span checks, bool isPacked) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* row = &Input[tid * Inputs]; + for (std::uint32_t index = 0; index < checks.size(); ++index) { + const std::uint32_t actual = Output[tid * Stride + index]; + const std::uint32_t expected = isPacked ? model.Packed(checks[index], row) : model.Scalar(checks[index], row); + Require(actual == expected, std::string("f16 min/max: ") + mode.name + ", " + checks[index].name + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + ++compared; + } + } + }; + run(scalar, ScalarCode, ScalarChecks, false); + if (packed) run(*packed, PackedCode, PackedChecks, true); + } + } + std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a4d392a4..5c8139908 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -145,6 +145,7 @@ private: bool dsBpermuteB32(const RdnaInstruction& inst); bool dsPermuteB32(const RdnaInstruction& inst); IrF32 selectF32(IrU1 condition, IrF32 trueValue, IrF32 falseValue); + IrU32 minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs); IrU32 convertF32ToU32Saturated(IrF32 value, float upperBound, float safeUpper, std::uint32_t highResult); IrU32 convertF32ToI32Saturated(IrF32 value, float lowerBound, float upperBound, float safeUpper, std::uint32_t lowerResult, std::uint32_t upperResult); IrU32 convertFlooredF32ToI32(IrF32 source, IrF32 floored); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 5bad7a11e..1a0bf7c6f 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -31,16 +31,17 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc } return applyF32ResultModifiers(inst.destination, clampF16Overflow(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})), {&lhs.Value(), &rhs.Value()})); }; - IrU32 result = packHalf2x16(translateLane(false), translateLane(true)); + const auto laneBits = [&](const RdnaOperand& operand, bool high) { + const IrU32 source = readF16SourceBits(operand); + const bool selectHigh = high ? operand.opSelHi : operand.opSel; + IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); + if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); + if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); + return bits; + }; + const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, laneBits(sourceAt(inst, 0u), high), laneBits(sourceAt(inst, 1u), high)); }; + IrU32 result = quietSnan ? packU16Lanes(minMaxLane(false), minMaxLane(true)) : packHalf2x16(translateLane(false), translateLane(true)); if (!inst.destination.clamp || !dx10Clamp() || quietSnan) { - const auto laneBits = [&](const RdnaOperand& operand, bool high) { - const IrU32 source = readF16SourceBits(operand); - const bool selectHigh = high ? operand.opSelHi : operand.opSel; - IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); - if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); - if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); - return bits; - }; const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; @@ -613,11 +614,19 @@ bool TranslationContext::ieeeMinMaxF32(const RdnaInstruction& inst, IrOpcode opc return true; } -bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { - struct Half { - IrValue* value; - IrU32 bits; +IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { + const auto order = [&](const IrU32& bits) -> IrValue& { + IrValue& value = ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&bits.Value(), &ir.Constant(0u), &ir.Constant(16u)}); + return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; + const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); + const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); + const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); + const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; + return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); +} + +bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { const bool ternary = opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32 || opcode == IrOpcode::FPMedTri32; const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); @@ -625,39 +634,34 @@ bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) const auto isSnan = [&](const IrU32& bits) { return IrU1(ir.LogicalAnd(isNan(bits).Value(), ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x0200u)), ir.Constant(0u)))); }; - const auto toBits = [&](IrValue& value) { return packHalf2x16(IrF32(value), IrF32(ir.ConstantF32(0.0f))); }; - std::array args{}; + std::array args{}; for (std::uint32_t index = 0u; index < (ternary ? 3u : 2u); ++index) { - const RdnaOperand& operand = sourceAt(inst, index); - args[index] = Half{&readF16AsF32(operand).Value(), readF16Bits(operand)}; + args[index] = readF16Bits(sourceAt(inst, index)); if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(args[index].bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); - const IrU32 sign(ir.BitwiseAnd(args[index].bits.Value(), ir.Constant(0x8000u))); - args[index].bits = IrU32(ir.Select(tiny.Value(), sign.Value(), args[index].bits.Value())); - args[index].value = &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&tiny.Value(), &ir.BitCastF32(ir.ShiftLeftLogical(sign.Value(), ir.Constant(16u))), args[index].value}); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(args[index].Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + args[index] = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(args[index].Value(), ir.Constant(0x8000u)), args[index].Value())); } } - const auto pair = [&](IrOpcode pairOpcode, const Half& lhs, const Half& rhs) { - IrValue& value = ir.Emit(pairOpcode, IrType::F32, {lhs.value, rhs.value}); - IrU32 bits(ir.Select(isNan(lhs.bits).Value(), rhs.bits.Value(), toBits(value).Value())); - bits = IrU32(ir.Select(isNan(rhs.bits).Value(), lhs.bits.Value(), bits.Value())); + const auto pair = [&](IrOpcode pairOpcode, const IrU32& lhs, const IrU32& rhs) { + IrU32 bits(ir.Select(isNan(lhs).Value(), rhs.Value(), minMaxF16Bits(pairOpcode == IrOpcode::FPMax32, lhs, rhs).Value())); + bits = IrU32(ir.Select(isNan(rhs).Value(), lhs.Value(), bits.Value())); if (ieeeMode) { - bits = IrU32(ir.Select(isSnan(rhs.bits).Value(), quietNan16(rhs.bits).Value(), bits.Value())); - bits = IrU32(ir.Select(isSnan(lhs.bits).Value(), quietNan16(lhs.bits).Value(), bits.Value())); + bits = IrU32(ir.Select(isSnan(rhs).Value(), quietNan16(rhs).Value(), bits.Value())); + bits = IrU32(ir.Select(isSnan(lhs).Value(), quietNan16(lhs).Value(), bits.Value())); } - return Half{&value, bits}; + return bits; }; IrU32 result; if (opcode == IrOpcode::FPMin32 || opcode == IrOpcode::FPMax32) { - result = pair(opcode, args[0], args[1]).bits; + result = pair(opcode, args[0], args[1]); } else if (opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32) { const auto pairOpcode = opcode == IrOpcode::FPMinTri32 ? IrOpcode::FPMin32 : IrOpcode::FPMax32; - result = pair(pairOpcode, pair(pairOpcode, args[0], args[1]), args[2]).bits; + result = pair(pairOpcode, pair(pairOpcode, args[0], args[1]), args[2]); } else { - const IrU1 anyNan(ir.LogicalOr(ir.LogicalOr(isNan(args[0].bits).Value(), isNan(args[1].bits).Value()), isNan(args[2].bits).Value())); - const Half minimum = pair(IrOpcode::FPMin32, pair(IrOpcode::FPMin32, args[0], args[1]), args[2]); - IrValue& median = ir.Emit(IrOpcode::FPMedTri32, IrType::F32, {args[0].value, args[1].value, args[2].value}); - result = IrU32(ir.Select(anyNan.Value(), minimum.bits.Value(), toBits(median).Value())); + const IrU1 anyNan(ir.LogicalOr(ir.LogicalOr(isNan(args[0]).Value(), isNan(args[1]).Value()), isNan(args[2]).Value())); + const IrU32 minimum = pair(IrOpcode::FPMin32, pair(IrOpcode::FPMin32, args[0], args[1]), args[2]); + const IrU32 median = minMaxF16Bits(true, minMaxF16Bits(false, args[0], args[1]), minMaxF16Bits(false, minMaxF16Bits(true, args[0], args[1]), args[2])); + result = IrU32(ir.Select(anyNan.Value(), minimum.Value(), median.Value())); } write16Bits(inst.destination, clampF16Bits(inst.destination, result)); return true; From 24894376ab070d7aa52de50dab6868ab23008abd Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:22:00 +0200 Subject: [PATCH 005/277] fix(recompiler): give wave64 vertex programs on 32-lane hosts a 32-lane s3 A wave64 vertex program runs at one guest lane per invocation, so on a 32-lane host subgroup its lanes 32-63 have no invocation. The vertex entry still set s3 = 64 | 64 << 8, telling the program that its wave holds 64 vertices and 64 primitives. An NGG culling program then keys a full-wave OR on lane < s3's primitive count, lanes 32-63 hold real cull results, and its v_readlane_b32 of lane 63 cannot be built (#2783). The entry now sets s3 = 32 | 32 << 8 for a wave64 program on a 32-lane host, the wave the host runs; the console launches waves with fewer vertices and primitives. The host subgroup width reaches the vertex input the way it reaches the compute and mesh inputs (BuildShaderStageInputInfo from HostSubgroupSize), and the source, prepared-shader and disk cache keys already carry it for every stage. Programs that do not read s3 are unchanged; wave32 programs and other host widths keep the full wave. The keys are then 0 in lanes 32-63, so the read is sound. On a 32-lane host, when the existing half-wave scan rules fail, the scan may also: - run under s_mov_b64 exec, -1 (EXEC constant true); - use bound_ctrl:1 row_shr steps when 0, the fill, is the identity (OR, add, unsigned max), also into another register, whose old value never shows since such a step writes every lane; - take keys shown to be 0 in lanes 32-63: built from 0 through selects under a lane id or v_mbcnt of -1 below at most 32, v_cmp_ne_u32 against 0, ORs, DPP and permlanes, in stages whose entry EXEC is every lane. Lane 63 is then the identity and lane 31 the subgroup reduction of the host's invocations. A shuffle from lane 31 is not enough: an RTX 5070 runs the vertex warps of a non-indexed triangle list with 30 of their 32 invocations. A v_readlane_b32 outside the host subgroup of another value that is 0 in lanes 32-63 reads 0. Only reads that used to throw change, besides lane 31 of such scans. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 90 ++++++++++ .../Optimization/ShaderStageInputInfo.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 154 +++++++++++++++--- .../Translation/src/InstructionTranslator.cpp | 3 +- .../src/ShaderInputInfoBuilder.cpp | 1 + docs/dev/TechnicalDebt.md | 4 +- 6 files changed, 229 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f..f3d847c19 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1356,6 +1356,95 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } +void verifyNarrowVertexWave() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; + const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x50000u, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = userDataBase; + if (userDataBase == 0u) request.context.userData = userData; + 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; + return request; + }; + const auto prepared = [&vertex](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { + return GetPreparedArtifact(*PrepareShader(vertex(code, 64u, subgroupSize, userDataBase))).spirv; + }; + 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) { + 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; + } + return count; + }; + const std::vector culling{ + 0xd7650015u, 0x000100c1u, 0xd7660015u, 0x00022ac1u, 0x7e0e0280u, 0x7da206f9u, 0x81060015u, 0x360e0a81u, + 0xbefe04c1u, 0xd4c50002u, 0x00020e80u, 0xd763000fu, 0x00022a81u, 0xd5010000u, 0x000a1e80u, + 0x380000fau, 0xff091100u, 0x380000fau, 0xff091200u, 0x380000fau, 0xff091400u, 0x380000fau, 0xff091800u, + 0xd778000fu, 0x03058300u, 0x38001f00u, 0xd7600002u, 0x00013f00u, 0xd760006bu, 0x00017f00u, 0x88026b02u, + 0x7e020202u, 0xf80008cfu, 0x01010101u, 0xbf810000u, + }; + const auto patched = [&culling](std::initializer_list> words) { + auto code = culling; + for (const auto& [index, word] : words) code[index] = word; + return code; + }; + const char* lane63 = "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup"; + require(reductions(prepared(culling, 32u)) == 1u, "narrow vertex wave: the NGG culling scan, keyed on lanes below the primitive count in s3, did not read lane 31 as a subgroup OR on 32 lanes"); + const auto wide64 = prepared(culling, 64u); + require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); + require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); + require(prepares(patched({{5, 0x7da82aa0u}, {6, 0xbf800000u}}), 32u), "narrow vertex wave: the scan keyed on lanes below 32 did not prepare on 32 lanes"); + expectFailure([&] { static_cast(prepares(patched({{5, 0x7da82aa1u}, {6, 0xbf800000u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on lanes below 33 read lane 63"); + expectFailure([&] { static_cast(prepares(culling, 32u, 0u)); }, lane63, "narrow vertex wave: a scan keyed on a count from user data read lane 63"); + expectFailure([&] { static_cast(prepares(patched({{6, 0x81060005u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on the vertex index instead of the lane read lane 63"); + auto otherRegister = patched({{21, 0x380200fau}, {24, 0x03058301u}, {25, 0x38001f01u}}); + otherRegister.insert(otherRegister.begin(), 0x7e020305u); + require(reductions(prepared(otherRegister, 32u)) == 1u, "narrow vertex wave: a scan whose bound_ctrl:1 step is written to another register did not read lane 31 as a subgroup OR on 32 lanes"); + otherRegister[23] = 0xff011800u; + expectFailure([&] { static_cast(prepares(otherRegister, 32u)); }, lane63, "narrow vertex wave: a scan step written to another register without bound_ctrl:1, which keeps that register's lanes outside the row, read lane 63"); + auto threeSteps = culling; + threeSteps.erase(threeSteps.begin() + 21, threeSteps.begin() + 23); + const auto partialScan = prepared(threeSteps, 32u); + require(!partialScan.empty() && reductions(partialScan) == 0u, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of 0 in lanes 32-63 did not read 0, or lane 31 became a subgroup OR"); + threeSteps[5] = 0x7da82aa1u; + threeSteps[6] = 0xbf800000u; + expectFailure([&] { static_cast(prepares(threeSteps, 32u)); }, lane63, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of lanes below 33 read 0"); + + std::array, 2> narrow; + std::array, 2> wide; + for (const bool narrowFirst : {false, true}) { + const std::vector code{0xbf800001u + static_cast(narrowFirst), 0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + const auto narrowRequest = vertex(code, 64u, 32u, 8u); + const auto wideRequest = vertex(code, 64u, 64u, 8u); + ShaderPreparationContext preparation; + const auto first = PrepareShader(narrowFirst ? narrowRequest : wideRequest, &preparation); + const auto second = PrepareShader(narrowFirst ? wideRequest : narrowRequest, &preparation); + const auto& narrowHandle = narrowFirst ? *first : *second; + const auto& wideHandle = narrowFirst ? *second : *first; + 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; + } + 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"); + const std::vector wave32{0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + require(GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 32u, 8u))).spirv == GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 64u, 8u))).spirv, "narrow vertex wave: s3 of a wave32 vertex program depended on the subgroup width"); +} + void verifyMeshConfiguration() { using namespace ShaderRecompiler; ShaderMeshInputInfo list; @@ -2939,6 +3028,7 @@ int main(int argc, char** argv) { verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); verifyHalfWaveReduction(); + verifyNarrowVertexWave(); verifyMeshConfiguration(); verifyPixelInputs(); verifyPixelRequestSerialization(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 6bc946acd..32300b030 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -127,6 +127,7 @@ struct ShaderVertexInputInfo { int fetchBufferReg = 0; int buffersNum = 0; std::uint32_t scratchSizeDwords = 0; + std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; ShaderClipSpaceTransform clipSpace; ShaderMeshInputInfo mesh; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dc3fa2a70..070247340 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -276,9 +276,13 @@ bool AllLanesBit(const IrValue* bit) { return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } +bool EveryInvocationEnters(IrShaderStage stage) { + return stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation; +} + class EntryLaneWalk { public: - explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation) {} + explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { bit = Resolved(bit); @@ -351,9 +355,107 @@ private: std::uint32_t budget = 512u; }; -bool RowShiftFlags(const IrValue* value, std::uint32_t control) { +bool EveryLaneBit(const IrValue* bit, bool constantTrue) { + const auto* resolved = Resolved(bit); + if (constantTrue && resolved != nullptr && resolved->HasImmediate()) return resolved->Type() == IrType::Bool && resolved->ImmediateBool(); + return AllLanesBit(resolved); +} + +bool WritesEveryLane(const IrValue& update) { + const auto flags = update.Flags(); + return flags.boundControl && flags.rowMask == 0xfu && flags.bankMask == 0xfu && EveryLaneBit(update.Argument(2), true); +} + +bool KeepsNoOldValue(const IrValue& permlane) { + const auto flags = permlane.Flags(); + return flags.fetchInactive || flags.boundControl || EveryLaneBit(permlane.Argument(3), true); +} + +bool UpperLaneTest(const IrValue* bit) { + const auto* compare = Resolved(bit); + return Is(compare, IrOpcode::UGreaterThanEqual32) && Is(Resolved(compare->Argument(0)), IrOpcode::LaneId) && IsU32(compare->Argument(1), 32u); +} + +class UpperLaneWalk { +public: + explicit UpperLaneWalk(IrShaderStage stage) : enters(EveryInvocationEnters(stage)), laneIndexed(enters && stage != IrShaderStage::TessellationControl) {} + + bool ZeroValue(const IrValue* value) { + value = Resolved(value); + if (!enters || value == nullptr || !spend()) return false; + if (value->HasImmediate()) return IsU32(value, 0u); + if (zeroValues.contains(value)) return true; + bool zero = false; + switch (value->Opcode()) { + case IrOpcode::SelectU32: zero = ZeroValue(value->Argument(2)) && (zeroBit(value->Argument(0)) || ZeroValue(value->Argument(1))); break; + case IrOpcode::BitwiseOr32: zero = ZeroValue(value->Argument(0)) && ZeroValue(value->Argument(1)); break; + case IrOpcode::DppMoveU32: zero = ZeroValue(value->Argument(0)); break; + case IrOpcode::DppUpdateU32: zero = ZeroValue(value->Argument(0)) && (WritesEveryLane(*value) || ZeroValue(value->Argument(1))); break; + case IrOpcode::Permlane16U32: zero = ZeroValue(value->Argument(0)) && (KeepsNoOldValue(*value) || ZeroValue(value->Argument(4))); break; + case IrOpcode::Phi: zero = incoming(value, [this](const IrValue* source) { return ZeroValue(source); }); break; + default: break; + } + if (zero && phis.empty()) zeroValues.insert(value); + return zero; + } + +private: + bool spend() { + if (budget == 0u) return false; + --budget; + return true; + } + + bool zeroBit(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr || !spend()) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + switch (bit->Opcode()) { + case IrOpcode::LogicalAnd: return zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1)); + case IrOpcode::SelectU1: return zeroBit(bit->Argument(2)) && (zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1))); + case IrOpcode::Phi: return incoming(bit, [this](const IrValue* source) { return zeroBit(source); }); + case IrOpcode::INotEqual32: return EitherOrder(bit, [this](const IrValue* value, const IrValue* zero) { return IsU32(zero, 0u) && ZeroValue(value); }); + case IrOpcode::ULessThan32: return belowUpperLanes(bit->Argument(0), bit->Argument(1)); + case IrOpcode::UGreaterThan32: return belowUpperLanes(bit->Argument(1), bit->Argument(0)); + default: return false; + } + } + + bool belowUpperLanes(const IrValue* index, const IrValue* bound) { + const auto* limit = Resolved(bound); + return limit != nullptr && limit->HasImmediate() && limit->Type() == IrType::U32 && limit->ImmediateU32() <= 32u && upperLaneIndex(index); + } + + bool upperLaneIndex(const IrValue* index) { + index = Resolved(index); + if (!laneIndexed || index == nullptr || !spend()) return false; + if (Is(index, IrOpcode::LaneId)) return true; + if (Is(index, IrOpcode::BitCount32)) { + const auto* mask = Resolved(index->Argument(0)); + return Is(mask, IrOpcode::SelectU32) && IsU32(mask->Argument(1), 0xffffffffu) && UpperLaneTest(mask->Argument(0)); + } + return Is(index, IrOpcode::IAdd32) && EitherOrder(index, [this](const IrValue* counted, const IrValue* added) { return Is(added, IrOpcode::BitCount32) && upperLaneIndex(counted); }); + } + + template + bool incoming(const IrValue* phi, TCheck&& check) { + if (!phis.insert(phi).second) return true; + bool result = phi->ArgumentCount() != 0u; + for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); + phis.erase(phi); + return result; + } + + bool enters; + bool laneIndexed; + std::unordered_set zeroValues; + std::unordered_set phis; + std::uint32_t budget = 1024u; +}; + +bool RowShiftFlags(const IrValue* value, std::uint32_t control, bool zeroFill) { const auto flags = value->Flags(); - return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && !flags.boundControl && !flags.fetchInactive; + return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && (!flags.boundControl || zeroFill) && !flags.fetchInactive; } const IrValue* Unflushed(const IrValue* value) { @@ -383,47 +485,54 @@ std::optional> ReductionOperands(const IrValue* co return operands; } -const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control) { - if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control) || !AllLanesBit(step->Argument(2))) return nullptr; - const auto* previous = Resolved(step->Argument(1)); +const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control, bool narrow) { + const bool zeroFill = narrow && reduction.identity == 0u && !reduction.floating; + if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control, zeroFill) || !EveryLaneBit(step->Argument(2), narrow)) return nullptr; + const auto* previous = zeroFill && WritesEveryLane(*step) ? nullptr : Resolved(step->Argument(1)); + const IrValue* input = nullptr; const auto operands = ReductionOperands(Resolved(step->Argument(0)), reduction); const bool matched = operands && EitherOrder(*operands, [&](const IrValue* moved, const IrValue* own) { - return own == previous && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control) && Resolved(moved->Argument(0)) == previous && AllLanesBit(moved->Argument(1)); + input = own; + return (previous == nullptr || own == previous) && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control, zeroFill) && Resolved(moved->Argument(0)) == own && EveryLaneBit(moved->Argument(1), narrow); }); - return matched ? previous : nullptr; + return matched ? input : nullptr; } -const IrValue* Unmasked(const IrValue* value) { - while (Is(value, IrOpcode::SelectU32) && AllLanesBit(value->Argument(0))) value = Resolved(value->Argument(1)); +const IrValue* Unmasked(const IrValue* value, bool narrow) { + while (Is(value, IrOpcode::SelectU32) && EveryLaneBit(value->Argument(0), narrow)) value = Resolved(value->Argument(1)); return value; } -bool CrossesRowsOf(const IrValue* value, const IrValue* scan) { - const auto* permlane = Unmasked(value); +bool CrossesRowsOf(const IrValue* value, const IrValue* scan, bool narrow) { + const auto* permlane = Unmasked(value, narrow); if (!Is(permlane, IrOpcode::Permlane16U32)) return false; const auto flags = permlane->Flags(); - return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && AllLanesBit(permlane->Argument(3)); + return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && EveryLaneBit(permlane->Argument(3), narrow); } -std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStage stage) { - const auto* combined = Unmasked(Resolved(value)); +std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* combined = Unmasked(Resolved(value), narrow); std::optional> operands; const auto reduction = std::find_if(WaveReductions.begin(), WaveReductions.end(), [&](const WaveReduction& candidate) { return (operands = ReductionOperands(combined, candidate)).has_value(); }); const IrValue* scan = nullptr; - if (reduction == WaveReductions.end() || !EitherOrder(*operands, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own); })) return std::nullopt; + if (reduction == WaveReductions.end() || !EitherOrder(*operands, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own, narrow); })) return std::nullopt; for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { - scan = RowScanStepInput(scan, *reduction, control); + scan = RowScanStepInput(scan, *reduction, control, narrow); if (scan == nullptr) return std::nullopt; } - const auto* source = Unmasked(scan); - if (!Is(source, IrOpcode::SelectU32) || !IsU32(source->Argument(2), reduction->identity) || !EntryLaneWalk{stage}.ZeroBit(source->Argument(0))) return std::nullopt; + const auto* source = Unmasked(scan, narrow); + const bool entryKeys = Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction->identity) && EntryLaneWalk{stage}.ZeroBit(source->Argument(0)); + const bool upperKeys = narrow && reduction->identity == 0u && !reduction->floating && UpperLaneWalk{stage}.ZeroValue(source); + if (!entryKeys && !upperKeys) return std::nullopt; return HalfWaveScan{&*reduction, source}; } std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { auto& state = ctx.state; if (state.program.WaveSize() != 64u || (lane & 31u) != 31u) return std::nullopt; - const auto scan = MatchHalfWaveScan(inst.Argument(0), state.program.Resources().stage); + const auto stage = state.program.Resources().stage; + auto scan = MatchHalfWaveScan(inst.Argument(0), stage, false); + if (!scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u) scan = MatchHalfWaveScan(inst.Argument(0), stage, true); if (!scan) return std::nullopt; if (scan->reduction->floating) { if (lane < 32u || state.hostSubgroupSize > 32u) return std::nullopt; @@ -851,7 +960,10 @@ std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { if (selector != nullptr && selector->HasImmediate() && state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u) { const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); if (const auto reduced = EmitHalfWaveReduction(ctx, inst, index)) return *reduced; - if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) { + if (index >= 32u && UpperLaneWalk{state.program.Resources().stage}.ZeroValue(inst.Argument(0))) return ConstantU32(state, 0u); + FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + } } const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, state.program.WaveSize() - 1u)); return ctx.Shuffle(inst, 0, lane); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 421ea460d..2539030a8 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -365,7 +365,8 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO } } else if (options.stage == ShaderStageKind::Vertex) { if (options.userDataBaseRegister >= 8u) { - entryIr.SetScalarReg(static_cast(3), entryIr.Constant(options.waveSize | (options.waveSize << 8u))); + const std::uint32_t lanes = options.waveSize == 64u && options.inputInfo.vertex->hostSubgroupSize == 32u ? 32u : options.waveSize; + entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::VertexIndex)); entryIr.SetVectorReg(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 18d89e989..79791905b 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -160,6 +160,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues auto& vertexStorage = HostThreadLocal(); vertexStorage = ShaderVertexInputInfo{}; vertexStorage.logicalStage = _toIrShaderStage(stage); + vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b72e13228..ce1379938 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -142,7 +142,7 @@ Throughout the project, every function at every stage either **does exactly what - [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws -- [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). 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. +- [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. 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). - [NGG vertex programs on the mesh path](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [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 - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. @@ -441,7 +441,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. -- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. +- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations: idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. - [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. From 98ce96a8b936d9b6d295aeb80cabbbd26d442abc Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:22:01 +0200 Subject: [PATCH 006/277] test(agc): run a wave64 NGG full-wave OR on a 32-lane device A wave64 vertex program with the culling scan of #2783 (lane < s3's primitive count, row_shr bound_ctrl:1 OR scan, v_permlanex16_b32, v_readlane_b32 of lanes 31 and 63) compares the wave OR with the ballot of its keys and collapses its triangle when they differ. On a 32-lane device every pixel of the grid must be covered. Before the s3 change the program does not build on 32 lanes; with a shuffle from lane 31 instead of the subgroup reduction no pixel is covered, as the device leaves lanes of each vertex warp idle. Other subgroup widths skip. Co-Authored-By: Claude Opus 5.5 --- .../execution/NggVertexWaveReduction.cpp | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp new file mode 100644 index 000000000..e5822da0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Columns = 7; +constexpr std::uint32_t Rows = 5; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xd7650015, 0x000100c1, 0xd7660015, 0x00022ac1, 0x7e0e0280, 0x7da206f9, 0x81060015, 0x360e0a81, + 0xbefe04c1, 0xd4c50002, 0x00020e80, 0xd763000f, 0x00022a81, 0xd5010000, 0x000a1e80, 0x380000fa, + 0xff091100, 0x380000fa, 0xff091200, 0x380000fa, 0xff091400, 0x380000fa, 0xff091800, 0xd778000f, + 0x03058300, 0x38001f00, 0x88040302, 0xd7600002, 0x00013f00, 0xd760006b, 0x00017f00, 0x88026b02, + 0xbf060402, 0x850580f2, 0xe0382000, 0x80020005, 0xbf8c3f70, 0x10000005, 0x10020205, 0x10040405, + 0x10060605, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +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::vector> Triangles() { + std::vector> vertices; + for (std::uint32_t cell = 0; cell < Columns * Rows; ++cell) { + const float left = -1.0f + 2.0f * static_cast(cell % Columns) / Columns; + const float right = -1.0f + 2.0f * static_cast(cell % Columns + 1u) / Columns; + const float bottom = -1.0f + 2.0f * static_cast(cell / Columns) / Rows; + const float top = -1.0f + 2.0f * static_cast(cell / Columns + 1u) / Rows; + for (const auto& corner : {std::array{left, bottom}, std::array{right, bottom}, std::array{right, top}, std::array{left, bottom}, std::array{right, top}, std::array{left, top}}) { + vertices.push_back({corner[0], corner[1], 0.5f, 1.0f}); + } + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 8, 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, {}}, + {64u, 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, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto actual = std::to_integer(Pixels[pixel * 4u]); + Require(actual == 255u, "NGG vertex wave reduction: pixel " + std::to_string(pixel) + " is " + std::to_string(actual) + ", expected 255 (a vertex whose wave OR of its lanes' bits, read from lanes 31 and 63, differs from its wave's ballot collapses its triangle)"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto subgroupSize = device->Target().subgroupSize; + if (subgroupSize != 32u) { + std::printf("skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has %u lanes\n", subgroupSize); + return VulkanTestSkipped; + } + if ((device->SubgroupStages() & VK_SHADER_STAGE_VERTEX_BIT) == 0u || !TargetHasCapability(device->Target(), spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic in vertex shaders"); + return VulkanTestSkipped; + } + for (std::uint32_t pass = 0; pass < 3; ++pass) { + Draw(*device); + Check(); + } + std::puts("NGG vertex wave reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 66236674f0fc7a77611aa1a6abd893403a1f6e48 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:26:45 +0200 Subject: [PATCH 007/277] fix(recompiler): keep lane 31 a shuffle without subgroup arithmetic The half-wave scan rules for 32-lane hosts made lane 31 of a matched scan a subgroup reduction and threw on a device without subgroup arithmetic, where the read used to build as a shuffle of the scan. Such a device now keeps the shuffle for lane 31; lane 63 still reads the identity, which needs no arithmetic. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 6 ++++++ .../SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp | 8 ++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index f3d847c19..6d946e7fa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1405,6 +1405,12 @@ void verifyNarrowVertexWave() { const auto wide64 = prepared(culling, 64u); require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + const auto otherKeys = patched({{7, 0x360e0a82u}}); + auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); + withoutArithmetic.target.supportedCapabilities = noArithmetic; + const auto shuffled = GetPreparedArtifact(*PrepareShader(withoutArithmetic)).spirv; + require(!shuffled.empty() && reductions(shuffled) == 0u, "narrow vertex wave: without subgroup arithmetic, the scan did not keep lane 31 as a shuffle with lane 63 read as 0"); require(prepares(patched({{5, 0x7da82aa0u}, {6, 0xbf800000u}}), 32u), "narrow vertex wave: the scan keyed on lanes below 32 did not prepare on 32 lanes"); expectFailure([&] { static_cast(prepares(patched({{5, 0x7da82aa1u}, {6, 0xbf800000u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on lanes below 33 read lane 63"); expectFailure([&] { static_cast(prepares(culling, 32u, 0u)); }, lane63, "narrow vertex wave: a scan keyed on a count from user data read lane 63"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 070247340..b674b1db9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -532,7 +532,8 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c if (state.program.WaveSize() != 64u || (lane & 31u) != 31u) return std::nullopt; const auto stage = state.program.Resources().stage; auto scan = MatchHalfWaveScan(inst.Argument(0), stage, false); - if (!scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u) scan = MatchHalfWaveScan(inst.Argument(0), stage, true); + const bool narrow = !scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u; + if (narrow) scan = MatchHalfWaveScan(inst.Argument(0), stage, true); if (!scan) return std::nullopt; if (scan->reduction->floating) { if (lane < 32u || state.hostSubgroupSize > 32u) return std::nullopt; @@ -540,7 +541,10 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c } const auto read = "v_readlane_b32 of lane " + std::to_string(lane) + " of a wave64 half-wave reduction scan"; const auto& capabilities = state.supportedCapabilities; - if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) { + if (narrow) return lane >= 32u ? std::optional(ConstantU32(state, scan->reduction->identity)) : std::nullopt; + ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + } if (state.hostSubgroupSize > 64u) ctx.Fail(inst, (read + " on a " + std::to_string(state.hostSubgroupSize) + "-lane host subgroup, which is wider than the wave").c_str()); const auto identity = ConstantU32(state, scan->reduction->identity); if (lane >= 32u && state.hostSubgroupSize <= 32u) return identity; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ce1379938..813fb013e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -441,7 +441,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. -- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations: idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. +- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. - [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. From dce174749d35485864e75eae630786a9f6944408 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:47:22 +0200 Subject: [PATCH 008/277] feat(libSceAmpr): implement _Cnd_signal and _Cnd_init_with_name Deathloop (PPSA01670), Minecraft Legends (PPSA05510) and GTA III Definitive Edition import both from libSceAmpr. They take a pointer to the handle slot and act on the conditions created by the libc _Cnd_init: _Cnd_signal wakes one waiter through a libc helper, _Cnd_init_with_name is _Cnd_init with the name ignored. _Thrd_id still throws. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAmpr/Export.cpp | 12 +-- core/libs/prx/libc/src/Threads.cpp | 5 ++ core/libs/tests/GuestAmprThreads.cpp | 120 +++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index c2c1cc811..fb835934f 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -234,6 +234,8 @@ static std::atomic_flag sharedPtrSpinLock; extern "C" { int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +int SignalCondition_nid_no_patch(void** handle); } static KernelTimespec XtimeDeadline(const Xtime& time) { @@ -895,18 +897,16 @@ int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_n return 0; } -int APS5_VABI _Cnd_signal_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); } -int APS5_VABI _Cnd_init_with_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +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) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 5399fd351..4565d8331 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -173,4 +173,9 @@ int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { return threadSuccess; } +int SignalCondition_nid_no_patch(void** handle) { + ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); + return threadSuccess; +} + } diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 81ea82946..6fa491548 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -24,6 +25,16 @@ int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char* name); +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 _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); } static constexpr int ThrdSuccess = 0; @@ -197,10 +208,119 @@ static void TestWcstombs() { Require(result == Failed); } +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +struct SignalContext { + void* condition = nullptr; + void* mutex = nullptr; + int waiting = 0; + int tokens = 0; + int consumed = 0; + int wakeups = 0; +}; + +static void Lock(SignalContext& context) { + Require(_Mtx_lock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void Unlock(SignalContext& context) { + Require(_Mtx_unlock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void* APS5_VABI ConsumeToken(void* arg) { + auto& context = *static_cast(arg); + Lock(context); + ++context.waiting; + while (context.tokens == 0) { + Require(_Cnd_wait_nid_postfix(&context.condition, &context.mutex) == ThrdSuccess); + ++context.wakeups; + } + --context.tokens; + ++context.consumed; + Unlock(context); + return nullptr; +} + +template +static void WaitUntil(SignalContext& context, TPredicate predicate) { + for (;;) { + Lock(context); + const bool done = predicate(); + Unlock(context); + if (done) return; + std::this_thread::yield(); + } +} + +static void Signal(SignalContext& context) { + Lock(context); + ++context.tokens; + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + Unlock(context); +} + +static void TestConditionSignal() { + SignalContext context; + context.condition = reinterpret_cast(std::uintptr_t{1}); + Require(_Cnd_init_with_name_nid_postfix(&context.condition, "AmprSignal") == ThrdSuccess && context.condition != nullptr); + Require(_Mtx_init_nid_postfix(&context.mutex, 0) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + + const auto single = Start(ConsumeToken, &context); + WaitUntil(context, [&] { return context.waiting == 1; }); + Signal(context); + Require(_Thrd_join_nid_postfix(single, nullptr) == ThrdSuccess); + Require(context.consumed == 1 && context.tokens == 0); + + context.waiting = 0; + context.consumed = 0; + context.wakeups = 0; + const std::array pair{Start(ConsumeToken, &context), Start(ConsumeToken, &context)}; + WaitUntil(context, [&] { return context.waiting == 2; }); + Signal(context); + WaitUntil(context, [&] { return context.consumed == 1; }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Lock(context); + Require(context.consumed == 1 && context.wakeups == 1 && context.tokens == 0); + Unlock(context); + Signal(context); + for (auto thread : pair) Require(_Thrd_join_nid_postfix(thread, nullptr) == ThrdSuccess); + Require(context.consumed == 2 && context.tokens == 0); + Require(_Cnd_broadcast_nid_postfix(&context.condition) == ThrdSuccess); + + RequireThrows([] { _Cnd_signal_nid_postfix(nullptr); }); + void* empty = nullptr; + RequireThrows([&] { _Cnd_signal_nid_postfix(&empty); }); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.mutex); }); + RequireThrows([] { _Cnd_init_with_name_nid_postfix(nullptr, "AmprSignal"); }); + _Cnd_destroy_nid_postfix(&context.condition); + Require(context.condition == nullptr); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.condition); }); + + void* unnamed = nullptr; + Require(_Cnd_init_with_name_nid_postfix(&unnamed, nullptr) == ThrdSuccess && unnamed != nullptr); + Require(_Cnd_signal_nid_postfix(&unnamed) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&unnamed); + void* plain = nullptr; + Require(_Cnd_init_nid_postfix(&plain) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&plain) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&plain); + _Mtx_destroy_nid_postfix(&context.mutex); +} + int main() { TestThreadJoin(); TestThreadSleep(); TestSpinLock(); TestWcstombs(); + TestConditionSignal(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b72e13228..074515d17 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -347,6 +347,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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`. - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, 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. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning +- [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` 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 - [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified From ae425ed920d441a618a594d1e6f693c8cccf845b Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Sat, 10 Oct 2026 14:09:24 +0200 Subject: [PATCH 009/277] feat(libSceTextToSpeech2): return EOPNOTSUPP from remaining speech exports Titles that call Open/Speak after a failed initialize should get the same unavailable-engine error instead of a throw. Co-authored-by: Cursor --- core/libs/prx/libSceTextToSpeech2/Export.cpp | 21 ++++++------------ core/libs/tests/GuestTextToSpeech2.cpp | 23 +++++++++++--------- docs/dev/TechnicalDebt.md | 6 ++--- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp index 8e344ce25..09950022b 100644 --- a/core/libs/prx/libSceTextToSpeech2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -12,8 +12,7 @@ constexpr std::int32_t SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0 extern "C" { int APS5_VABI sceTextToSpeech2GetSystemStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { @@ -22,33 +21,27 @@ std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { } int APS5_VABI sceTextToSpeech2Cancel() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Close() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2GetSpeechStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Speak() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Terminate() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } } diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index f3ee2c80e..9bac0096e 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -3,27 +3,30 @@ #include #include #include -#include extern "C" { std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param); int APS5_VABI sceTextToSpeech2Open(); +int APS5_VABI sceTextToSpeech2Cancel(); +int APS5_VABI sceTextToSpeech2Close(); +int APS5_VABI sceTextToSpeech2GetSpeechStatus(); +int APS5_VABI sceTextToSpeech2Speak(); +int APS5_VABI sceTextToSpeech2Terminate(); +int APS5_VABI sceTextToSpeech2GetSystemStatus(); } int main() { + constexpr std::int32_t unsupported = static_cast(0x8002002D); const std::uint32_t param[12]{0x2000000, 0x26c}; - if (sceTextToSpeech2Initialize(param) != static_cast(0x8002002D)) { + if (sceTextToSpeech2Initialize(param) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } - bool threw = false; - try { - sceTextToSpeech2Open(); - } catch (const std::runtime_error&) { - threw = true; - } - if (!threw) { - std::puts("TextToSpeech2: open after failed initialization did not throw"); + if (sceTextToSpeech2Open() != unsupported || sceTextToSpeech2Cancel() != unsupported || + sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus() != unsupported || + sceTextToSpeech2Speak() != unsupported || sceTextToSpeech2Terminate() != unsupported || + sceTextToSpeech2GetSystemStatus() != unsupported) { + std::puts("TextToSpeech2: later calls did not report the unsupported operation"); std::abort(); } std::puts("TextToSpeech2 tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b72e13228..739503c07 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -34,7 +34,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutAddVrrStatusFlagsPrivilege](../../core/libs/prx/libSceVideoOut/src/Output.cpp) returns 0 and does nothing: the host output has no VRR state to report. The name comes from an assert message in a title that requires `SCE_VIDEO_OUT_OK`; the NID hashes to it, the arguments are unknown (it is called without any) - [sceVideoOutVrrPegToFixedRate, sceVideoOutVrrUnpegFromFixedRate](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05684 passes the handle to both, two further zero arguments to `sceVideoOutVrrPegToFixedRate`, and ignores the results. The meaning of those two arguments is unknown, so only zeros are accepted. VRR is not modelled and the output always runs at a fixed rate, so both calls only validate the handle and return 0 - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output -- [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` fails and the other exports throw. PPSA01317 skips its narration set-up when initialization fails +- [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` and the other exports return `EOPNOTSUPP`. PPSA01317 skips its narration set-up when initialization fails - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. @@ -89,7 +89,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list - [sceVoiceChatRequestCreateVoiceChatGroup, sceVoiceChatRequestJoinVoiceChatGroup, sceVoiceChatRequestLeaveVoiceChatGroup, sceVoiceChatRequestDeleteVoiceChatGroup](../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - assumed to take a request id and two pointers like the other `sceVoiceChatRequest*` calls; offline, every argument is ignored and `SIGNED_OUT` is returned, as for those - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown -- [sceTextToSpeech2Initialize](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters +- [sceTextToSpeech2Initialize](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters and return the same error - [sceSystemServiceLaunchWebBrowser](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown - [Command buffer cursors](../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [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 @@ -282,7 +282,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [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 -- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature +- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title From 6199661506b8ca0265fe17e86a5ef814d53930fe Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:04:11 -0400 Subject: [PATCH 010/277] test(recompiler): verify BDA store boundary faults and written pages --- .../prx/libSceAgcDriver/tests/BdaDevice.cpp | 4 +- .../libSceAgcDriver/tests/BdaExecution.cpp | 73 +++++++++++++++++++ .../prx/libSceAgcDriver/tests/BdaShader.cpp | 17 ++++- .../prx/libSceAgcDriver/tests/BdaShader.hpp | 2 + 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 8d73dada8..860643bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -162,10 +162,12 @@ int main(int argc, char** argv) { buffer.Bytes()[255] = std::byte{0x5a}; Require(buffer.Bytes()[255] == std::byte{0x5a}, "real BDA buffer mapping failed"); if (device->RunsOnCpu()) { - std::cout << "CPU Vulkan device: BDA execution not tested\n"; + std::cout << "CPU Vulkan device: BDA read and multi-dword write execution not tested\n"; } else { RunBdaExecutionTests(device->GetContext()); } + RunBdaStoreExecutionTests(device->GetContext()); + std::cout << "BDA store boundary tests passed\n"; RunColorTransferTests(device->GetContext()); std::cout << "Vulkan BDA allocation and execution tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index bf7674cc4..5fc40da4e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -1,10 +1,13 @@ #include "BdaShader.hpp" #include "BdaAbi.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include #include +#include #include +#include #include #include @@ -330,3 +333,73 @@ void RunBdaExecutionTests(const Context& context) { run(guest, 8, 0, Abi::FaultReason::Permission); RunBdaDwordWriteTests(context); } + +void RunBdaStoreExecutionTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + Buffer first(context, 2, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer second(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer table(context, sizeof(Abi::Header) + 2 * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, Abi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t guest = 0x7fff1235fffdULL; + constexpr std::uint32_t writable = Abi::Read | Abi::Write; + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::array ranges{}; + const auto run = [&](const char* name, std::uint64_t address, std::uint32_t bits, bool coherent, const std::array& expected, std::initializer_list writtenAddresses, Abi::FaultReason reason = {}, std::uint64_t faultAddress = 0, std::uint32_t faultBytes = 0) { + const std::string label = std::string("BDA store ") + name + (coherent ? " coherent: " : ": "); + std::memset(first.Bytes().data(), 0xcd, first.Bytes().size()); + std::memset(second.Bytes().data(), 0xcd, second.Bytes().size()); + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0, output.Bytes().size()); + Pipeline pipeline(context, MakeBdaStoreTestShader(address, 0xa1b2c3d4u, bits, coherent), {&table, &fault, &output}); + pipeline.Run(1); + std::array actual{}; + std::memcpy(actual.data(), first.Bytes().data(), 2); + std::memcpy(actual.data() + 2, second.Bytes().data(), 3); + Require(actual == expected, label + "incorrect stored bytes or writes outside the mapped span"); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + if (reason == Abi::FaultReason{}) { + Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault"); + } else { + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata"); + } + std::array words{}; + std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); + Require(words[Abi::WrittenOverflowWord] == 0, label + "written-page table overflowed"); + std::set recordedPages; + Require(Abi::ForEachWrittenRange(words.data(), [&](std::uint64_t address, std::size_t bytes) { + for (std::size_t offset = 0; offset < bytes; offset += std::size_t{1} << Abi::WrittenPageShift) { + Require(recordedPages.insert((address + offset) >> Abi::WrittenPageShift).second, label + "written-page table contains duplicate pages"); + } + }), label + "invalid written-page record"); + std::set expectedPages; + std::set expectedChunks; + for (const auto writtenAddress : writtenAddresses) { + expectedPages.insert(writtenAddress >> Abi::WrittenPageShift); + expectedChunks.insert(Abi::WrittenChunkKey(writtenAddress)); + } + Require(recordedPages == expectedPages, label + "incorrect written-page set"); + Require(words[Abi::WrittenCountWord] == expectedChunks.size(), label + "incorrect written-chunk claim count"); + std::uint32_t completed = 0; + std::memcpy(&completed, output.Bytes().data(), sizeof(completed)); + Require(completed == 1, label + "store did not finish its per-byte accesses"); + }; + for (const bool coherent : {false, true}) { + ranges = {{{guest, guest + 2, first.DeviceAddress(), writable, 0}, {guest + 2, guest + 5, second.DeviceAddress(), writable, 0}}}; + run("byte", guest + 1, 8, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}); + run("split short", guest + 1, 16, coherent, {0xcd, 0xd4, 0xc3, 0xcd, 0xcd}, {guest + 1, guest + 2}); + run("split unaligned dword", guest, 32, coherent, {0xd4, 0xc3, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}); + ranges[1].permissions = Abi::Read; + run("read-only byte", guest + 2, 8, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only short suffix", guest + 1, 16, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only suffix", guest, 32, coherent, {0xd4, 0xc3, 0xcd, 0xcd, 0xcd}, {guest}, Abi::FaultReason::Permission, guest + 2, 1); + ranges[0].end = guest + 1; + ranges[1].permissions = writable; + run("unmapped middle byte", guest, 32, coherent, {0xd4, 0xcd, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}, Abi::FaultReason::Unmapped, guest + 1, 1); + ranges = {{{guest + 3, guest + 5, first.DeviceAddress(), writable, 0}, {guest + 5, guest + 8, second.DeviceAddress(), writable, 0}}}; + run("split aligned dword", guest + 3, 32, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Unmapped, guest + 3, 4); + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index efc43683d..529457a60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -3,6 +3,9 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif namespace { @@ -40,7 +43,7 @@ std::vector MakeShader(std::uint64_t address, bool coherent, bool state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.module.AllocateId()); SpirvValueEmitContext ctx(state); - auto& instruction = extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); + auto& instruction = writes ? program.CreateValue(IrOpcode::StoreAddressU32, IrType::Void) : extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); IrBuilder ir(program); for (std::uint32_t dword = 0; dword < 4u; ++dword) { if ((extracted & (1u << dword)) == 0u) continue; @@ -106,3 +109,15 @@ std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std return std::vector(values.begin(), values.begin() + dwords); }); } + +std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent) { + using namespace ShaderRecompiler; + auto shader = MakeShader(address, coherent, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + EmitBdaStore(ctx, instruction, base, ConstantU32(ctx.state, value), bits); + return std::vector{ConstantU32(ctx.state, 1u)}; + }, 0u, true); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shader = ValidateAndOptimizeSpirv(shader, VK_API_VERSION_1_1, shader[1], false, false); +#endif + return shader; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 27c99edba..abfefe977 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -9,6 +9,8 @@ std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_ std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); +std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); From c9038f7d8daad962c4aed9d57f8815385a13d5f3 Mon Sep 17 00:00:00 2001 From: Winvoker Date: Sun, 11 Oct 2026 15:05:41 +0300 Subject: [PATCH 011/277] fix(progress): match stub calls across whitespace --- docs/dev/PROGRESS.md | 2 +- tools/progress.py | 10 ++++++---- tools/tests/test_progress.py | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index dec716d9f..ca20cad09 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -4,7 +4,7 @@ ## Library counts -The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Other definitions are counted as implemented. Braces inside string literals, character literals and comments do not delimit bodies. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). +The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Call matching uses complete identifiers and accepts whitespace or comments before `(`. Other definitions are counted as implemented. Braces inside string literals, character literals and comments do not delimit bodies. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). A library with no scanned definitions can include a sibling library's source builder. The scanner recognizes literal `include(${CMAKE_CURRENT_SOURCE_DIR}/..//.cmake)` references, including quoted paths. It does not evaluate arbitrary CMake expressions. It also recognizes a top-level `.cpp` that includes another library's source with `#include "prx//.cpp"`. diff --git a/tools/progress.py b/tools/progress.py index 6f37f6cce..e3b281bb8 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -13,6 +13,7 @@ ISA = Path(__file__).resolve().parent / "rdna_isa.txt" SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5")}/blob/main' DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") STUB = "NotImplemented_nid_no_patch" +CALL = re.compile(r"\b(\w+)\s*\(") CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', re.M) SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) STUB_WRAPPER = re.compile(r"\bstatic\s+(?:\[\[noreturn\]\]\s+)?[\w:<>,\s*&]+?\s+(\w+)\s*\([^;{]*\)\s*\{") @@ -91,10 +92,11 @@ def body_end(text, start): def stub_calls(text): - calls = [STUB] + calls = {STUB} for match in STUB_WRAPPER.finditer(text): - if STUB in text[match.end() - 1:body_end(text, match.end() - 1)]: - calls.append(match.group(1) + "(") + body = text[match.end() - 1:body_end(text, match.end() - 1)] + if any(call.group(1) == STUB for call in CALL.finditer(body)): + calls.add(match.group(1)) return calls @@ -156,7 +158,7 @@ def scan_library(path): if name.endswith("_nid_no_patch"): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call in body for call in calls): + if any(call.group(1) in calls for call in CALL.finditer(body)): todo.add(name) continue done.add(name) diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index a68933827..d5ee4c87a 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -20,6 +20,30 @@ class ProgressTests(unittest.TestCase): self.assertEqual(group["done_names"], []) self.assertEqual(group["todo_names"], ["sceExampleSupportFormat"]) + def test_stub_wrapper_calls_allow_whitespace(self): + for separator in ("", " ", "\t", "\n", " /* separator */ "): + with self.subTest(separator=separator), tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Unsupported() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + f"int APS5_VABI Pending() {{ return Unsupported{separator}(); }}\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], []) + self.assertEqual(group["todo_names"], ["Pending"]) + + def test_stub_wrapper_names_match_whole_identifiers(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "static int Unsupported() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "static int NotUnsupported(int value) { return value * 2; }\n" + "int APS5_VABI Ready(int value) { return NotUnsupported(value); }\n") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Ready"]) + self.assertEqual(group["todo_names"], []) + def test_braces_in_literals_and_comments_do_not_delimit_bodies(self): with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "libSceExample" From 6aa0b4fb22d1745755c6bba983503a8801a27f3f Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 11 Oct 2026 08:03:23 -0400 Subject: [PATCH 012/277] fix(libkernel_sys): create symlinks with CreateSymbolicLinkW on Windows --- core/libs/prx/libkernel_sys/Export.cpp | 41 +++++++++++++++++++++----- core/libs/tests/GuestKernelSys.cpp | 14 ++++++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 630e46d51..f477e9aa2 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -16,6 +16,15 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif +#endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -27,9 +36,6 @@ constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; -#ifdef _WIN32 -constexpr int WindowsPrivilegeNotHeld = 1314; -#endif std::atomic creationMask{022}; @@ -70,6 +76,13 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (*path == '\0') return Fail(GuestEnoent); if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); const auto native = ResolvePath_nid_no_patch(path); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + NotImplemented_nid_no_patch("readlink of a Windows symbolic link"); + return -1; + } +#endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); if (error) return Fail(GuestError(error)); @@ -123,17 +136,31 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { } const auto native = ResolvePath_nid_no_patch(created); std::error_code error; +#ifdef _WIN32 + if (GetFileAttributesW(native.wstring().c_str()) != INVALID_FILE_ATTRIBUTES) return static_cast(Fail(GuestEexist)); +#else if (std::filesystem::exists(std::filesystem::symlink_status(native, error))) return static_cast(Fail(GuestEexist)); +#endif if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(GuestEnoent)); const std::filesystem::path contents(target); - if (std::filesystem::is_directory(native.parent_path() / contents, error)) + const bool directory = std::filesystem::is_directory(native.parent_path() / contents, error); +#ifdef _WIN32 + DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (directory) flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + if (!CreateSymbolicLinkW(native.wstring().c_str(), std::filesystem::path(contents).make_preferred().wstring().c_str(), flags)) { + const DWORD code = GetLastError(); + if (code == ERROR_PRIVILEGE_NOT_HELD) return static_cast(Fail(GuestEperm)); + if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast(Fail(GuestEexist)); + if (code == ERROR_PATH_NOT_FOUND) return static_cast(Fail(GuestEnoent)); + throw std::system_error(static_cast(code), std::system_category(), "symlink"); + } +#else + if (directory) std::filesystem::create_directory_symlink(contents, native, error); else std::filesystem::create_symlink(contents, native, error); -#ifdef _WIN32 - if (error.category() == std::system_category() && error.value() == WindowsPrivilegeNotHeld) return static_cast(Fail(GuestEperm)); -#endif if (error) throw std::system_error(error, "symlink"); +#endif RecordWrittenPath_nid_no_patch(native); return 0; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a34349584..38ea7bb25 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif #ifdef __linux__ #include #include @@ -130,13 +133,22 @@ int main() { const auto symbolic = (root / "symbolic").string(); const int created = symlink_nid_postfix("original.bin", symbolic.c_str()); if (created == 0) { + const auto dangling = (root / "dangling").string(); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(symbolic).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); + Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); +#else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); Require(std::filesystem::file_size(symbolic) == std::filesystem::file_size(original)); Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); - const auto dangling = (root / "dangling").string(); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && !std::filesystem::exists(dangling)); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); +#endif } else { Require(created == -1 && Error() == 1); std::fprintf(stderr, "symbolic links unavailable: the symlink success cases not tested\n"); From 8355b1f7b8f1709a10ad08b5fb4fbe0954ba7643 Mon Sep 17 00:00:00 2001 From: BharatYadav007 Date: Sun, 11 Oct 2026 16:47:28 +0530 Subject: [PATCH 013/277] feat(libc): implement setlogmask --- core/libs/prx/libc/src/Syslog.cpp | 24 +++++++++++++----- core/libs/tests/GuestSyslog.cpp | 42 +++++++++++++++++++++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/Syslog.cpp b/core/libs/prx/libc/src/Syslog.cpp index c7cc00b3e..e582d236e 100644 --- a/core/libs/prx/libc/src/Syslog.cpp +++ b/core/libs/prx/libc/src/Syslog.cpp @@ -19,6 +19,7 @@ constexpr int priorityMask = 0x07; constexpr int facilityMask = 0x3f8; constexpr int userFacility = 1 << 3; constexpr int internalLogPriority = 3 | 0x02 | 0x20 | 0x01; +int logMask = 0xff; std::mutex& OutputMutex() { static std::mutex mutex; @@ -26,7 +27,6 @@ std::mutex& OutputMutex() { } void Emit(int priority, const std::string& message) { - std::lock_guard lock(OutputMutex()); std::fprintf(stderr, "[syslog:%d] %s", priority, message.c_str()); if (message.empty() || message.back() != '\n') std::fputc('\n', stderr); std::fflush(stderr); @@ -75,16 +75,28 @@ std::string Format(const char* format, VaList* args) { } extern "C" { +int APS5_VABI setlogmask_nid_postfix(int mask) { + std::lock_guard lock(OutputMutex()); + const int previous = logMask; + if (mask != 0) logMask = mask; + return previous; +} + void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* args) { const int saved = *__error_nid_postfix(); + std::lock_guard lock(OutputMutex()); if (priority & ~(priorityMask | facilityMask)) { - char complaint[64]; - std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); - Emit(internalLogPriority, complaint); + if (logMask & (1 << (internalLogPriority & priorityMask))) { + char complaint[64]; + std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); + Emit(internalLogPriority, complaint); + } priority &= priorityMask | facilityMask; } - if ((priority & facilityMask) == 0) priority |= userFacility; - Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + if (logMask & (1 << (priority & priorityMask))) { + if ((priority & facilityMask) == 0) priority |= userFacility; + Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + } *__error_nid_postfix() = saved; } diff --git a/core/libs/tests/GuestSyslog.cpp b/core/libs/tests/GuestSyslog.cpp index a4634c5d6..3f0270eea 100644 --- a/core/libs/tests/GuestSyslog.cpp +++ b/core/libs/tests/GuestSyslog.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef _WIN32 #include #else @@ -10,6 +11,7 @@ extern "C" { void APS5_VABI syslog_nid_postfix(int, const char*, ...); +int APS5_VABI setlogmask_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,11 +27,38 @@ int main() { Require(saved >= 0 && ::dup2(::fileno(captured), ::fileno(stderr)) == ::fileno(stderr)); #endif *__error_nid_postfix() = 2; + Require(setlogmask_nid_postfix(0) == 0xff); syslog_nid_postfix(6, "station %s at %d kbps", "radio", 128); const int afterFirst = *__error_nid_postfix(); syslog_nid_postfix(16 | 3, "open: %m, %%m kept, %d%%\n", 50); syslog_nid_postfix(0x10000 | 4, "masked"); syslog_nid_postfix(7, ""); + Require(setlogmask_nid_postfix(1 << 3) == 0xff); + Require(setlogmask_nid_postfix(0) == (1 << 3)); + int count = -1; + syslog_nid_postfix(7, "suppressed %m%n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + syslog_nid_postfix(16 | 3, "allowed: %m"); + Require(*__error_nid_postfix() == 2); + syslog_nid_postfix(0x10000 | 7, "suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(1 << 4) == (1 << 3)); + syslog_nid_postfix(0x10000 | 4, "warning without diagnostic"); + syslog_nid_postfix(0x10000 | 7, "both suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(0x100) == (1 << 4)); + Require(setlogmask_nid_postfix(0) == 0x100); + syslog_nid_postfix(0x10000 | 3, "no priorities enabled %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(-1) == 0x100); + Require(setlogmask_nid_postfix(0) == -1); + syslog_nid_postfix(7, "negative mask"); + Require(setlogmask_nid_postfix(0x55) == -1); + for (int priority = 0; priority < 8; ++priority) + syslog_nid_postfix(16 | priority, "level %d", priority); + std::thread([] { Require(setlogmask_nid_postfix(0xff) == 0x55); }).join(); + Require(setlogmask_nid_postfix(0) == 0xff); + syslog_nid_postfix(7, "mask shared across threads"); std::fflush(stderr); #ifdef _WIN32 Require(::_dup2(saved, ::_fileno(stderr)) == 0 && ::_close(saved) == 0); @@ -38,14 +67,23 @@ int main() { #endif Require(afterFirst == 2 && *__error_nid_postfix() == 2); std::rewind(captured); - char text[512]{}; + char text[1024]{}; const auto length = std::fread(text, 1, sizeof(text) - 1, captured); const char* expected = "[syslog:14] station radio at 128 kbps\n" "[syslog:19] open: No such file or directory, %m kept, 50%\n" "[syslog:35] syslog: unknown facility/priority: 10004\n" "[syslog:12] masked\n" - "[syslog:15] \n"; + "[syslog:15] \n" + "[syslog:19] allowed: No such file or directory\n" + "[syslog:35] syslog: unknown facility/priority: 10007\n" + "[syslog:12] warning without diagnostic\n" + "[syslog:15] negative mask\n" + "[syslog:16] level 0\n" + "[syslog:18] level 2\n" + "[syslog:20] level 4\n" + "[syslog:22] level 6\n" + "[syslog:15] mask shared across threads\n"; Require(length == std::strlen(expected) && std::strcmp(text, expected) == 0); Require(std::fclose(captured) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..cf1010701 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -581,7 +581,7 @@ Throughout the project, every function at every stage either **does exactly what - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws - [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. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported -- [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`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied +- [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`, and errno is preserved. `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. `openlog` and `closelog` are not implemented, so their identity and options are not applied - [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)) - [sigtimedwait](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not From a553e226388620c0fa55cbc7dcf2b20df049423e Mon Sep 17 00:00:00 2001 From: Mouad Date: Sun, 11 Oct 2026 12:13:49 +0000 Subject: [PATCH 014/277] feat(agc): write memory for WRITE_DATA with DST_SEL 3 Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding turns into WRITE_DATA with DST_SEL 3 and a label address in the title's memory that RELEASE_MEM and WAIT_REG_MEM then use. The validator rejected DST_SEL 3 as the GDS; it now writes guest memory like DST_SEL 5, resolves as a label, and the title runs past its first frame. TechnicalDebt records where the semantics come from. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 9c53b7f6e8ce2ca81d8f0fa551d12632acfeb60d) --- core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index f3b0dea8f..9b7b06e0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -453,7 +453,7 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() >= 5, "WRITE_DATA has no data"); require((packet[1] & ~(0x40110f00u | WriteDataCachePolicy)) == 0, "WRITE_DATA engine or reserved fields are not implemented"); const auto destination = (packet[1] >> 8u) & 0xfu; - require(destination == 1 || destination == 2 || destination == 5, "WRITE_DATA register or GDS destination is not implemented"); + require(destination == 1 || destination == 2 || destination == 3 || destination == 5, "WRITE_DATA register destination is not implemented"); require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination"); break; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 74b7c1c60..5c6394789 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -362,6 +362,12 @@ void testMemory() { check(data[0] == 61 && data[1] == 62, "WRITE_DATA with a cache policy failed"); execute(state, makePacket(0x37, {0x40100500, low(data.data()), high(data.data()), 81, 82})); check(data[0] == 81 && data[1] == 82, "WRITE_DATA to memory from the graphics queue failed"); + execute(state, makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 91, 92})); + check(data[0] == 91 && data[1] == 92, "WRITE_DATA with destination 3 did not write memory"); + const auto fence = makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 0}); + const auto fenceLabel = AgcDriver::Pm4::DecodeLabelWrite(fence); + check(fenceLabel.has_value() && fenceLabel->address == reinterpret_cast(data.data()), "WRITE_DATA with destination 3 did not resolve as a label"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x100000, low(data.data()), high(data.data()), 1}), 0); }, "register destination"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x08000100, low(data.data()), high(data.data()), 71}), 0); }, "reserved"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x80000100, low(data.data()), high(data.data()), 51}), 0); }, "engine"); execute(state, makePacket(0x83, {0xbff8, 2, low(data.data()), high(data.data())})); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f2d5680f4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -461,6 +461,7 @@ Throughout the project, every function at every stage either **does exactly what - A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. +- [WRITE_DATA with DST_SEL 3](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. - The [shader float mode](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. With f16/f64 denormals flushed the hardware also flushes denormal inputs of f64 min/max, which those lowerings still do not. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - [v_ldexp_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. From 06f84010bb82a4be475e213df7441c4833053b36 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:52 +0530 Subject: [PATCH 015/277] feat(libc): implement reallocf Two catalog titles stop on the absent libSceLibcInternal export reallocf (YMZO9ChZb0E) on main 0f863c10: - PPSA97510 PSXS5 2.2.0 - PPSA98510 SwanStationPS5 1.1.3 FAIL: unresolved ELF import YMZO9ChZb0E from libSceLibcInternal.prx reallocf follows FreeBSD 11 lib/libc/stdlib/reallocf.c (freebsd-src stable/11 5ed7eb0d97ba4436218e810f61bd059acba984c2) on top of the guest realloc: it reallocates through the application heap, and when realloc fails (the application allocator returns null or the default heap rejects the size) it frees the block through the same allocator, sets ENOMEM and returns null. Size 0 frees and returns null as realloc does here, so the block is not freed twice. Other heap failures throw, as for realloc. The NID matches tools/nid_names.py and aerolib. The application heap tests cover grow, shrink, a null pointer, size 0 and failure with both the replacement and the default allocator. Co-Authored-By: screamm Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/FileAndHeap.cpp | 13 +++++++++++++ core/libs/tests/ApplicationHeap.cpp | 26 ++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 40 insertions(+) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..7234a1cef 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -284,6 +286,17 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); } +void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { + try { + return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + ApplicationHeapFree_nid_no_patch(ptr); + errno = 12; + return nullptr; +} + void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { return ApplicationHeapAlign_nid_no_patch(alignment, size); } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 6cd531139..04adba57c 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void* APS5_VABI reallocf_nid_postfix(void*, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; @@ -148,6 +149,24 @@ int main(int argc, char** argv) { pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); + pointer = static_cast(reallocf_nid_postfix(nullptr, 24)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24); + std::memset(pointer, 0x3c, 24); + pointer = static_cast(reallocf_nid_postfix(pointer, 200)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200); + for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c); + pointer = static_cast(reallocf_nid_postfix(pointer, 9)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); + for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); @@ -211,7 +230,14 @@ int main(int argc, char** argv) { require(frees == 6); _ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64); require(frees == 6); + require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7); fail = true; + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12); + reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); }); + require(frees == 8); require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..83fa934c2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -185,6 +185,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [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` - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_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 - [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 From d4301cf9b384f312ffebdf511f516f5a17821dbf Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 16:25:54 +0300 Subject: [PATCH 016/277] fix(libkernel): parse and print inet text addresses by the guest rules inet_pton and inet_ntop no longer call the host resolver library, whose accepted forms and output differ from the guest's (leading zeros, zone ids, IPv4-embedded printing). (cherry picked from commit 058d808ae7c43042d0ffd5fad76062a2df16b871) --- .../Socket/src/AddressConversion.cpp | 230 +++++++++++++++--- core/libs/tests/GuestAddressConversion.cpp | 45 ++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 246 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index b10fb9633..1ed1a2e20 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -1,47 +1,217 @@ -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#else -#include -#endif #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include extern "C" int* APS5_VABI __error_nid_postfix(); +namespace { +constexpr int kGuestInet = 2; +constexpr int kGuestInet6 = 28; +constexpr std::size_t kMaxText = 46; + +bool ParseDecimalPart(const char* begin, const char* end, std::uint8_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 3) return false; + if (length > 1 && *begin == '0') return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + if (*cursor < '0' || *cursor > '9') return false; + total = total * 10 + static_cast(*cursor - '0'); + } + if (total > 255) return false; + value = static_cast(total); + return true; +} + +bool ParseQuad(const char* begin, const char* end, std::uint8_t (&bytes)[4]) { + const char* partBegin = begin; + int parts = 0; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != '.') continue; + if (parts == 4 || !ParseDecimalPart(partBegin, cursor, bytes[parts])) return false; + ++parts; + if (cursor == end) break; + partBegin = cursor + 1; + } + return parts == 4; +} + +int HexValue(char symbol) { + if (symbol >= '0' && symbol <= '9') return symbol - '0'; + if (symbol >= 'a' && symbol <= 'f') return symbol - 'a' + 10; + if (symbol >= 'A' && symbol <= 'F') return symbol - 'A' + 10; + return -1; +} + +bool ParseHexGroup(const char* begin, const char* end, std::uint16_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 4) return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + const int digit = HexValue(*cursor); + if (digit < 0) return false; + total = total * 16 + static_cast(digit); + } + value = static_cast(total); + return true; +} + +bool ParseGroupList(const char* begin, const char* end, bool mayEndWithQuad, + std::uint16_t (&groups)[8], int& count) { + count = 0; + if (begin == end) return true; + const char* groupBegin = begin; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != ':') continue; + const bool last = cursor == end; + bool hasDot = false; + for (const char* probe = groupBegin; probe != cursor; ++probe) hasDot = hasDot || *probe == '.'; + if (hasDot) { + std::uint8_t quad[4]; + if (!last || !mayEndWithQuad || count > 6 || !ParseQuad(groupBegin, cursor, quad)) return false; + groups[count++] = static_cast(quad[0] << 8 | quad[1]); + groups[count++] = static_cast(quad[2] << 8 | quad[3]); + return true; + } + if (count == 8 || !ParseHexGroup(groupBegin, cursor, groups[count])) return false; + ++count; + if (last) return true; + groupBegin = cursor + 1; + } +} + +bool TextToV6(const char* text, std::uint8_t (&bytes)[16]) { + const char* const end = text + std::strlen(text); + const char* gap = std::strstr(text, "::"); + std::uint16_t head[8]; + std::uint16_t tail[8]; + int headCount = 0; + int tailCount = 0; + if (!gap) { + if (!ParseGroupList(text, end, true, head, headCount) || headCount != 8) return false; + } else { + if (std::strstr(gap + 1, "::")) return false; + if (!ParseGroupList(text, gap, false, head, headCount)) return false; + if (!ParseGroupList(gap + 2, end, true, tail, tailCount)) return false; + if (headCount + tailCount > 7) return false; + } + std::uint16_t words[8]{}; + for (int index = 0; index < headCount; ++index) words[index] = head[index]; + for (int index = 0; index < tailCount; ++index) words[8 - tailCount + index] = tail[index]; + for (int index = 0; index < 8; ++index) { + bytes[index * 2] = static_cast(words[index] >> 8); + bytes[index * 2 + 1] = static_cast(words[index]); + } + return true; +} + +std::size_t AppendDecimal(char* out, unsigned value) { + char digits[3]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +std::size_t FormatQuad(const std::uint8_t* bytes, char* out) { + std::size_t length = 0; + for (int index = 0; index < 4; ++index) { + if (index) out[length++] = '.'; + length += AppendDecimal(out + length, bytes[index]); + } + return length; +} + +std::size_t AppendHex(char* out, unsigned value) { + static const char symbols[] = "0123456789abcdef"; + char digits[4]; + std::size_t count = 0; + do { + digits[count++] = symbols[value & 15]; + value >>= 4; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +void FindLongestZeroRun(const std::uint16_t (&words)[8], int& start, int& length) { + start = -1; + length = 0; + for (int from = 0; from < 8; ++from) { + if (words[from] != 0) continue; + int run = 0; + while (from + run < 8 && words[from + run] == 0) ++run; + if (run > length) { + start = from; + length = run; + } + from += run; + } + if (length < 2) { + start = -1; + length = 0; + } +} + +std::size_t V6ToText(const std::uint8_t* bytes, char* out) { + std::uint16_t words[8]; + for (int index = 0; index < 8; ++index) + words[index] = static_cast(bytes[index * 2] << 8 | bytes[index * 2 + 1]); + int runStart; + int runLength; + FindLongestZeroRun(words, runStart, runLength); + std::size_t length = 0; + if (runStart == 0 && (runLength == 6 || (runLength == 5 && words[5] == 0xffff))) { + const char* prefix = runLength == 6 ? "::" : "::ffff:"; + length = std::strlen(prefix); + std::memcpy(out, prefix, length); + return length + FormatQuad(bytes + 12, out + length); + } + for (int index = 0; index < 8;) { + if (index == runStart) { + out[length++] = ':'; + if (index + runLength == 8) out[length++] = ':'; + index += runLength; + continue; + } + if (index) out[length++] = ':'; + length += AppendHex(out + length, words[index]); + ++index; + } + return length; +} +} + extern "C" { int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return -1; } + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return -1; } if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } - unsigned char address[16]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const int result = InetPtonA(nativeFamily, text, address); -#else - const int result = ::inet_pton(nativeFamily, text, address); -#endif - if (result == 1) std::memcpy(destination, address, family == 2 ? 4 : 16); - else if (result == -1) *__error_nid_postfix() = 22; - return result; + if (family == kGuestInet) { + std::uint8_t quad[4]; + if (!ParseQuad(text, text + std::strlen(text), quad)) return 0; + std::memcpy(destination, quad, sizeof(quad)); + return 1; + } + std::uint8_t address[16]; + if (!TextToV6(text, address)) return 0; + std::memcpy(destination, address, sizeof(address)); + return 1; } const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return nullptr; } + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return nullptr; } if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } - char text[INET6_ADDRSTRLEN]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const auto* result = InetNtopA(nativeFamily, const_cast(source), text, sizeof(text)); -#else - const auto* result = ::inet_ntop(nativeFamily, source, text, sizeof(text)); -#endif - if (!result) { *__error_nid_postfix() = 22; return nullptr; } - const auto size = std::strlen(text) + 1; - if (size > capacity) { *__error_nid_postfix() = 28; return nullptr; } - std::memcpy(destination, text, size); + char text[kMaxText + 1]; + const auto* bytes = static_cast(source); + const std::size_t length = family == kGuestInet ? FormatQuad(bytes, text) : V6ToText(bytes, text); + text[length] = '\0'; + if (length + 1 > capacity) { *__error_nid_postfix() = 28; return nullptr; } + std::memcpy(destination, text, length + 1); return destination; } } diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 458ec1b13..675da4ed6 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -31,4 +31,49 @@ int main() { Require(__inet_pton_nid_postfix(2, "256.1.2.3", address.data()) == 0); Require(__inet_pton_nid_postfix(28, "::ffff:192.0.2.17", address.data()) == 1); Require(address[10] == 255 && address[11] == 255 && address[12] == 192 && address[15] == 17); + + for (const char* rejected : {"01.2.3.4", "1.2.3", "1.2.3.4.5", "1.2.3.4.", " 1.2.3.4", "0x1.2.3.4", "1..3.4", "1.2.3.-4"}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(2, rejected, address.data()) == 0); + Require(address[0] == 0xA5); + } + for (const char* rejected : {"::ffff:01.2.3.4", "::ffff:1.2.3.04", "1:2:3:4:5:6:7:8::", "::1:2:3:4:5:6:7:8", "1::2::3", ":::", ":1::", "1::2:", ":1:2:3:4:5:6:7", + "1:2:3:4:5:6:7", "1:2:3:4:5:6:7:8:9", "12345::", "fe80::1%eth0", "1.2.3.4::", "::1.2.3.4:1", "::g", "1:2:3:4:5:6:1.2.3.4:7", + "1:2:3:4:5:6:7:1.2.3.4", ""}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(28, rejected, address.data()) == 0); + Require(address[0] == 0xA5 && address[15] == 0xA5); + } + Require(__inet_pton_nid_postfix(28, "1:2:3:4:5:6:7::", address.data()) == 1); + Require(address[13] == 7 && address[14] == 0 && address[1] == 1); + Require(__inet_pton_nid_postfix(28, "ABCD:ef01::1", address.data()) == 1); + Require(address[0] == 0xAB && address[1] == 0xCD && address[2] == 0xEF && address[3] == 0x01 && address[15] == 1); + + struct Text { const char* bytesAsText; const char* expected; }; + const Text samples[] = { + {"::ffff:0.0.0.1", "::ffff:0.0.0.1"}, {"::ffff:0:1:0", "::ffff:0:1:0"}, {"::1", "::1"}, {"::", "::"}, {"1::", "1::"}, + {"::2", "::2"}, {"::1.2.3.4", "::1.2.3.4"}, {"1:0:0:2:0:0:0:3", "1:0:0:2::3"}, {"1:0:2:0:3:0:4:0", "1:0:2:0:3:0:4:0"}, + {"0:0:1:0:0:2:0:0", "::1:0:0:2:0:0"}, {"::ffff:1.2.3.4", "::ffff:1.2.3.4"}, {"2001:DB8::17", "2001:db8::17"}}; + for (const Text& sample : samples) { + Require(__inet_pton_nid_postfix(28, sample.bytesAsText, address.data()) == 1); + char output[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), output, sizeof(output)) == output); + Require(std::strcmp(output, sample.expected) == 0); + const auto length = std::strlen(sample.expected); + std::memset(output, 'x', sizeof(output)); + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length + 1)) == output); + Require(std::strcmp(output, sample.expected) == 0); + std::memset(output, 'x', sizeof(output)); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length)) == nullptr); + Require(*__error_nid_postfix() == 28 && output[0] == 'x'); + } + address.fill(0xFF); + char widest[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), widest, sizeof(widest)) == widest); + Require(std::strcmp(widest, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") == 0); + const unsigned char fourth[4] = {255, 0, 7, 100}; + Require(__inet_ntop_nid_postfix(2, fourth, widest, 12) == widest && std::strcmp(widest, "255.0.7.100") == 0); + Require(__inet_ntop_nid_postfix(2, fourth, widest, 11) == nullptr && *__error_nid_postfix() == 28); + Require(__inet_ntop_nid_postfix(10, fourth, widest, sizeof(widest)) == nullptr && *__error_nid_postfix() == 47); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..62790703d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -339,6 +339,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies +- [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 - [gethostbyname](../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes From 5550ca63dd00b769e33e6037db8260f4d9b2f71f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 12:31:03 +0000 Subject: [PATCH 017/277] fix(agc): wait for shutdown at shader ABI entrypoints --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- .../Execution/src/Driver/EntryPoints.cpp | 12 +++++++++--- .../prx/libSceAgcDriver/tests/WaitShutdown.cpp | 17 +++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..1d89b426c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -313,8 +313,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_wait_shutdown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_GPU_WAIT_TIMEOUT_MS=20" TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_resolution_shutdown COMMAND agc_driver_wait_shutdown_tests --resolution-shutdown) + set_tests_properties(agc_driver_resolution_shutdown PROPERTIES TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_wait_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_wait_shutdown agc_driver_resolution_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 277f9673d..54f524103 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -123,13 +123,19 @@ extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) { AgcDriver::ReportFailure(error); } -extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { +extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp index 5bfd23f7f..72c88cbc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp @@ -1,9 +1,11 @@ #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libc/include/Shutdown.hpp" #include #include #include +#include #include #include #include @@ -22,6 +24,10 @@ void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } +void DelayExit() { + std::this_thread::sleep_for(std::chrono::seconds(1)); +} + std::array WriteData(volatile std::uint32_t* destination) { const auto address = static_cast(reinterpret_cast(destination)); return {0xc0033700, 0x00100200, static_cast(address), static_cast(address >> 32u), 1}; @@ -59,7 +65,7 @@ void CheckShutdownCancellation() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::seconds(1)); for (const auto& state : states) { Require(state.label == 0, "unsatisfied label was unexpectedly signalled"); Require(state.after == 0 && state.queued == 0, "unsatisfied wait completed before shutdown"); @@ -77,7 +83,14 @@ void CheckShutdownCancellation() { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--resolution-shutdown") == 0) { + AgcDriverWaitIdle_nid_postfix(); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + AgcDriverResolveShaderAbi_nid_postfix(nullptr, {}, {}); + return 1; + } try { CheckShutdownCancellation(); std::puts("unsatisfied label waits cancelled cleanly at shutdown"); From b316c9885d80370e413efb70e1a21b9024d8c4a5 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 12:11:43 +0000 Subject: [PATCH 018/277] fix(libc): resume host foreign exceptions with host unwinder --- core/libs/prx/libc/src/exception/Unwind.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index b958b18f8..e38aa9fb1 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -1,7 +1,11 @@ #include "prx/libc/include/exceptions/Unwind.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "prx/libc/src/specifics/x86_64/RegisterContext.cpp" +#if defined(__linux__) +#include +#endif #ifdef _WIN32 #include #include @@ -502,6 +506,14 @@ _Unwind_Reason_Code APS5_VABI _Unwind_RaiseException_nid_postfix(_Unwind_Excepti } [[noreturn]] void APS5_VABI _Unwind_Resume_nid_postfix(_Unwind_Exception* exception) { +#if defined(__linux__) + if (!LibcException::Native(exception->exception_class) && !exception->private_1) { + using Resume = void (*)(_Unwind_Exception*); + const auto resume = reinterpret_cast(dlsym(RTLD_NEXT, "_Unwind_Resume")); + if (!resume) std::abort(); + resume(exception); + } +#endif _Unwind_Context context; LibcCaptureRegisters(context.registers); if (!LibcUnwind::Step(context)) std::abort(); From 859736aa0ed2aaee5a9db2cb4914e90f0d40325c Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Sun, 11 Oct 2026 01:18:32 -0700 Subject: [PATCH 019/277] fix(net): preserve truncated UDP receives on Windows --- core/libs/prx/libSceNet/CMakeLists.txt | 12 ++ core/libs/prx/libSceNet/Export.cpp | 8 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 8 +- core/libs/tests/GuestDatagramReceive.cpp | 146 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestDatagramReceive.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index aa2b08ccd..bcf7a8ccf 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -3,6 +3,18 @@ target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) + target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_datagram_receive_tests) + foreach(api IN ITEMS recv recvfrom sceNetRecv sceNetRecvfrom) + add_test(NAME guest_datagram_${api} COMMAND guest_datagram_receive_tests ${api}) + set_tests_properties(guest_datagram_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_datagram_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0775ad06b..7e909a505 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -645,6 +645,9 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } const int native_flags = flags == 2 ? MSG_PEEK : 0; 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); +#endif return result >= 0 ? result : fail(native_error()); } @@ -663,8 +666,11 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); const int native_flags = flags == 2 ? MSG_PEEK : 0; - const int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + 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 + if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); +#endif if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b5f852307..4d1866b4d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -659,6 +659,9 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); +#endif return result < 0 ? Fail(NativeError()) : result; } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { @@ -750,8 +753,11 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((!buffer && length) || (address && !addressLength)) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); - const auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), + auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); +#endif if (result < 0) return Fail(NativeError()); if (address && socket->family == GuestUnix) { if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp new file mode 100644 index 000000000..22cf4caaf --- /dev/null +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -0,0 +1,146 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#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 connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static void Check(bool net, bool from, int family) { + const auto create = [&] { return net ? sceNetSocket(nullptr, family, 2, 17) : socket_nid_postfix(family, 2, 17); }; + const int receiver = create(), sender = create(); + Require(receiver >= 0 && sender >= 0, "create UDP sockets"); + std::array destination{}; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const auto bind = [&](int descriptor) { + return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : + bind_nid_postfix(descriptor, destination.data(), addressSize); + }; + Require(bind(receiver) == 0 && bind(sender) == 0, "bind loopback sockets"); + auto senderAddress = destination; + std::uint32_t size = addressSize; + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "get sender address"); + size = addressSize; + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "get receiver address"); + Require((net ? sceNetConnect(receiver, senderAddress.data(), addressSize) : + connect_nid_postfix(receiver, senderAddress.data(), addressSize)) == 0, "connect receiver"); + const int enabled = 1; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : + fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto receive = [&](void* buffer, std::size_t length, int flags, void* address, std::uint32_t* addressLength) { + if (net) return from ? sceNetRecvfrom(receiver, buffer, length, flags, address, addressLength) : + sceNetRecv(receiver, buffer, length, flags); + return from ? recvfrom_nid_postfix(receiver, buffer, length, flags, address, addressLength) : + recv_nid_postfix(receiver, buffer, length, flags); + }; + const auto send = [&](const char* data, std::size_t length) { + Require((net ? sceNetSendto(sender, data, length, 0, destination.data(), addressSize) : + sendto_nid_postfix(sender, data, length, 0, destination.data(), addressSize)) == + static_cast(length), "send datagram"); + }; + const char message[] = "UDP header/body"; + for (const std::size_t capacity : {std::size_t{4}, sizeof(message), sizeof(message) + 4, std::size_t{0}}) { + send(message, sizeof(message)); + std::array buffer{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(buffer.data(), buffer.size(), 2, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(message), "wait for complete datagram"); + for (const int flags : {2, 0}) { + buffer.fill('!'); + std::array address{}; + address.fill(0xa5); + size = address.size(); + *error = 123; + result = receive(buffer.data() + 1, capacity, flags, address.data(), &size); + const auto expected = std::min(capacity, sizeof(message)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s%s IPv%d capacity=%zu flags=%d: expected %zu, got %lld errno=%d\n", + net ? "sceNetRecv" : "recv", from ? "from" : "", family == 2 ? 4 : 6, + capacity, flags, expected, static_cast(result), *error); + std::exit(1); + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[0] == '!' && std::memcmp(buffer.data() + 1, message, expected) == 0 && + std::all_of(buffer.begin() + 1 + expected, buffer.end(), [](char value) { return value == '!'; }), + "copied prefix and buffer guards"); + if (from) { + Require(size == addressSize && std::memcmp(address.data(), senderAddress.data(), addressSize) == 0, + "source address on truncated receive"); + Require(std::all_of(address.begin() + addressSize, address.end(), [](unsigned char value) { return value == 0xa5; }), + "source address guards"); + } + if (flags == 2) { + Require(receive(buffer.data(), buffer.size(), 2, nullptr, nullptr) == sizeof(message) && + std::memcmp(buffer.data(), message, sizeof(message)) == 0, "short peek preserves complete packet"); + } + } + Require(receive(buffer.data(), buffer.size(), 0, nullptr, nullptr) < 0 && *error == 35, + "consuming receive discards remainder"); + } + send("", 0); + char byte = '!'; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(&byte, 1, 0, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == 0 && byte == '!', "empty datagram"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected receive API argument"); + const bool net = std::strncmp(argv[1], "sce", 3) == 0; + const bool from = std::strstr(argv[1], "from") != nullptr; + if (net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Check(net, from, 2); + Check(net, from, 28); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..7b287afaa 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -554,6 +554,7 @@ Throughout the project, every function at every stage either **does exactly what - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [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` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [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. - [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. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `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 - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [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 From 9e8b5a21bcc597e44ac5be5fa940ee765f3a923f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 12:35:51 +0000 Subject: [PATCH 020/277] fix(shader): allow constant-zero sRGB comparisons --- .../recompiler/Optimization/src/ResourceMaterializer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 838e52932..dd0aaded9 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -192,7 +192,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } } decoded.srgbDecode = !storage && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; - if (decoded.srgbDecode && !base.srgbDecodeCompatible) { + if (decoded.srgbDecode && !base.srgbDecodeCompatible && !base.depthCompare) { throw std::runtime_error("samples or gathers an sRGB image the device cannot sample, which is not implemented"); } if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { @@ -735,7 +735,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } - if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !storage && !image.depthCompare && !image.packed) { + if (image.srgbDecodeFormats != 0u && (image.srgbDecodeCompatible || image.depthCompare) && !storage && !image.packed) { const auto count = modes.size(); for (std::size_t index = 0; index < count; ++index) { const auto& base = modes[index]; From 8b0b78f529b0d20084f05d80638ee68450aceea3 Mon Sep 17 00:00:00 2001 From: Abdulrahman Elkordy <196347165+Silverash-i8@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:09:42 +0300 Subject: [PATCH 021/277] test(libkernel): don't require restoring the directory time on Windows The empty-path utimes check restores the working directory's write time afterwards and requires the restore to succeed. With MinGW, std::filesystem::last_write_time fails with EACCES when setting a directory's time (a file's works), so guest_filesystem aborts there on Windows. The restore stays, only its result is no longer checked. Claude-Session: https://claude.ai/code/session_012SLQbhAiexxW6TtjYToW8a --- core/libs/tests/GuestFilesystem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..23b5ac7fd 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -283,7 +283,6 @@ int main() { const int emptyUtimes = sceKernelUtimes_nid_postfix("", nullptr); std::error_code restoreTimeError; std::filesystem::last_write_time(".", cwdWriteTime, restoreTimeError); - Require(!restoreTimeError); Require(emptyUtimes == static_cast(0x80020002u)); Require(stat_nid_postfix(presentName.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(missingName.c_str(), &status) == -1 && *__error_nid_postfix() == 2); From a9575dc70b928e0924d1a3c9d603e656fef96e0e Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 11:43:58 +0000 Subject: [PATCH 022/277] fix(agc): normalize disabled user VGPR special offset --- .../libSceAgc/Shader/include/ShaderConstants.hpp | 1 + core/libs/prx/libSceAgc/Shader/src/PrimState.cpp | 10 ++++++++-- core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp | 13 +++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp index 14c803b8f..4248c29fd 100644 --- a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp +++ b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp @@ -19,6 +19,7 @@ constexpr std::uint32_t VGT_SHADER_STAGES_EN = 0x2D5u; constexpr std::uint32_t VGT_PRIMITIVE_TYPE = 0x242u; constexpr std::uint32_t GE_CNTL = 0x25Bu; +constexpr std::uint32_t GE_USER_VGPR1 = 0x25Cu; constexpr std::uint32_t GE_USER_VGPR_EN = 0x262u; constexpr std::uint32_t GE_PC_ALLOC = 0x260u; diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 9963ae2db..e03b491b2 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -29,7 +29,13 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ if (!validStages(gs) || (hs != nullptr && !validStages(hs))) { throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); } - if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) { + const auto normalizeUserVgprEnable = [](ShaderRegister reg) { + if (reg.offset == ShaderRegs::GE_USER_VGPR1 && reg.value == 0) reg.offset = ShaderRegs::GE_USER_VGPR_EN; + return reg; + }; + const auto gsUserVgprEnable = normalizeUserVgprEnable(gs->specials->ge_user_vgpr_en); + const auto hsUserVgprEnable = hs != nullptr ? normalizeUserVgprEnable(hs->specials->ge_user_vgpr_en) : gsUserVgprEnable; + if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN)) { throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets"); } std::array contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type}; @@ -44,7 +50,7 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ } else { contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; } - std::array primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; + std::array primitiveValues{gs->specials->ge_cntl, hsUserVgprEnable, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index c02780707..2dce67750 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -120,6 +120,19 @@ int main() { } } } + ShaderSpecialRegs disabledVgprSpecial = special; + disabledVgprSpecial.ge_user_vgpr_en = {GE_USER_VGPR1, 0}; + vertex.specials = &disabledVgprSpecial; + preparations = 0; + std::array normalizedPrimitive{}; + Require(sceAgcCreatePrimState(nullptr, normalizedPrimitive.data(), nullptr, &vertex, 7) == 0); + Require(preparations == 1 && preparedPrimitive[1].offset == GE_USER_VGPR_EN && preparedPrimitive[1].value == 0); + disabledVgprSpecial.ge_user_vgpr_en.value = 1; + bool rejectedEnabledVgprAlias = false; + try { static_cast(sceAgcCreatePrimState(nullptr, normalizedPrimitive.data(), nullptr, &vertex, 7)); } + catch (const std::runtime_error&) { rejectedEnabledVgprAlias = true; } + Require(rejectedEnabledVgprAlias); + vertex.specials = &special; preparations = 0; std::array patchContext{}; std::array patchPrimitive{}; From b33d2e3736ec1dfe6e416838a88a9b1422b8dae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:53:51 +0200 Subject: [PATCH 023/277] docs(debt): record the disabled user VGPR enable alias in sceAgcCreatePrimState Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..04123a3c1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -149,6 +149,7 @@ Throughout the project, every function at every stage either **does exactly what - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. +- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - a `ge_user_vgpr_en` special at `GE_USER_VGPR1` (0x25c) with value 0 is accepted and returned at `GE_USER_VGPR_EN` (0x262), as hand-built shader headers of PPSA99039 store it (issue #3457); the console's behaviour is not known. The same offset with a non-zero value throws - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws From 0e9a8dd949efe7738ec963dffa2590917f50c94b Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 11 Oct 2026 08:50:20 -0400 Subject: [PATCH 024/277] feat(libkernel): export execv --- core/libs/prx/libkernel/System/src/Process.cpp | 7 +++++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..4ee238cd1 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -288,6 +288,13 @@ int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { return -1; } +int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { + (void)path; + (void)arguments; + NotImplemented_nid_no_patch("execv: executable replacement"); + return -1; +} + int APS5_VABI sceKernelGetCurrentCpu(void) { #ifdef _WIN32 PROCESSOR_NUMBER processor{}; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..c0ae298f2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -411,6 +411,7 @@ Throughout the project, every function at every stage either **does exactly what - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. - [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [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. - [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. - [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, but files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. From a61e2d35ddaa7ae92cdf05f9ef310a507dbbaf05 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 10 Oct 2026 23:13:22 -0300 Subject: [PATCH 025/277] fix(libSceSysmodule): return INVALID_VALUE for module id 0 Measured on a PS5 (firmware 13.60): sceSysmoduleIsLoaded, sceSysmoduleLoadModule and sceSysmoduleUnloadModule with id 0, and the Internal and InternalWithArg calls with id 0 or 0x80000000, return 0x805A1000 and leave the WithArg result untouched. They threw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 13 +++++++------ core/libs/tests/GuestSysmodule.cpp | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 0dd8ea293..8ed923947 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -20,6 +20,7 @@ extern "C" void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); namespace { +constexpr int SCE_SYSMODULE_ERROR_INVALID_VALUE = static_cast(0x805A1000); constexpr int SCE_SYSMODULE_ERROR_UNLOADED = static_cast(0x805A1001); constexpr int kRtldNow = 2; @@ -117,7 +118,7 @@ int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleIsLoaded: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -134,7 +135,7 @@ int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleLoadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -151,7 +152,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, (void)argv; (void)unk; if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternalWithArg: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternalWithArg: unknown id ") + std::to_string(id)); @@ -166,7 +167,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleUnloadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -183,7 +184,7 @@ int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternal: unknown id ") + std::to_string(id)); @@ -195,7 +196,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleUnloadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternal: unknown id ") + std::to_string(id)); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4a63bbd37..8c24e9a17 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #ifdef __linux__ @@ -17,6 +18,7 @@ int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id); int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id); int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id); +int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, ModuleInfoForUnwind* info); int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle); int APS5_VABI sceKernelDlsym(std::int32_t handle, const char* symbol, void** addr); @@ -38,7 +40,9 @@ bool ThrowsRuntimeError(TFunction function) { constexpr std::uint16_t kFiberModuleId = 0x0006; constexpr std::uint16_t kUltModuleId = 0x0007; +constexpr int kInvalidValue = static_cast(0x805A1000); constexpr int kModuleUnloaded = static_cast(0x805A1001); +constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; #ifdef __linux__ @@ -98,6 +102,17 @@ void CheckUnwindInfo() { } int main() { + Require(sceSysmoduleIsLoaded(0) == kInvalidValue); + Require(sceSysmoduleLoadModule(0) == kInvalidValue); + Require(sceSysmoduleUnloadModule(0) == kInvalidValue); + for (std::uint32_t id : {0u, kInternalFlag}) { + Require(sceSysmoduleLoadModuleInternal(id) == kInvalidValue); + Require(sceSysmoduleUnloadModuleInternal(id) == kInvalidValue); + int result = 0x5a5a5a5a; + Require(sceSysmoduleLoadModuleInternalWithArg(id, 0, nullptr, 0, &result) == kInvalidValue); + Require(result == 0x5a5a5a5a); + } + Require(sceSysmoduleIsLoaded(kFiberModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kFiberModuleId) == 0); Require(sceSysmoduleIsLoaded(kFiberModuleId) == 0); From bdf67fcc495fe716870c2a6ba28799d0c3e20747 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 10 Oct 2026 23:12:17 -0300 Subject: [PATCH 026/277] feat(libs): declare the PS VR2 exports PPSA15878 imports Propagation: Paradise Hotel (PPSA15878) does not load on main: its executable imports 15 PS VR2 functions that no library exports, the first reported being sceVrTracker2GetPlayAreaOrientedBoundingBox (snYs7Nf-RKk). Declare the nine libSceHmd2 reprojection and gaze exports, three libSceVrTracker2 exports and three scePadVrController trigger effect exports, without parameters, as throwing stubs. Names come from the aerolib NID list. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHmd2/Export.cpp | 45 +++++++++++++++++++++++ core/libs/prx/libScePad/Export.cpp | 15 ++++++++ core/libs/prx/libSceVrTracker2/Export.cpp | 15 ++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 76 insertions(+) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 2e1740373..9cfafc35a 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -21,6 +21,11 @@ int APS5_VABI sceHmd2Close() { return 0; } +int APS5_VABI sceHmd2GazeGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { NotImplemented_nid_no_patch(__func__); return 0; @@ -46,16 +51,36 @@ int APS5_VABI sceHmd2ReprojectionBeginFrame() { return 0; } +int APS5_VABI sceHmd2ReprojectionDisableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionDisableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionEnableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionEnableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { NotImplemented_nid_no_patch(__func__); return 0; @@ -81,6 +106,16 @@ int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { return 0; } +int APS5_VABI sceHmd2ReprojectionSetMirroringOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer() { NotImplemented_nid_no_patch(__func__); return 0; @@ -91,6 +126,16 @@ int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { return 0; } +int APS5_VABI sceHmd2ReprojectionSetTiming() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2SetVibration() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 38bb59ca5..b14f0cd70 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -256,6 +256,11 @@ int APS5_VABI scePadVrControllerRead() { return 0; } +int APS5_VABI scePadVrControllerGetTriggerEffectState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadVrControllerReadState(int handle, void* state) { (void)handle; (void)state; @@ -263,6 +268,16 @@ int APS5_VABI scePadVrControllerReadState(int handle, void* state) { return 0; } +int APS5_VABI scePadVrControllerSetTriggerEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetTriggerEffects() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (remote == nullptr) return PAD_ERROR_INVALID_ARG; diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 34024033d..75a4fce48 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -5,6 +5,16 @@ extern "C" { +int APS5_VABI sceVrTracker2Finalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2GetResult() { NotImplemented_nid_no_patch(__func__); return 0; @@ -15,6 +25,11 @@ int APS5_VABI sceVrTracker2Initialize() { return 0; } +int APS5_VABI sceVrTracker2LocateCoordinateSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2QueryMemory() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..3f3d6f248 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -72,6 +72,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceHmd2Initialize](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero - PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list +- PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 - [NGS2 ATRAC9 waveform loops](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. From bf4cb4974da94be584ab1eccbd836a1975548496 Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 11 Oct 2026 08:52:48 -0400 Subject: [PATCH 027/277] feat(libScePosixForWebKit): implement fnmatch --- .../prx/libScePosixForWebKit/CMakeLists.txt | 9 ++ core/libs/prx/libScePosixForWebKit/Export.cpp | 147 ++++++++++++++++++ core/libs/tests/GuestFnmatch.cpp | 75 +++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 232 insertions(+) create mode 100644 core/libs/tests/GuestFnmatch.cpp diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index c786173f6..fddddc543 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -17,6 +17,15 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fnmatch_tests ${LIBS_INCLUDE_DIR}/tests/GuestFnmatch.cpp) + target_include_directories(guest_fnmatch_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fnmatch_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_fnmatch_tests) + add_test(NAME guest_fnmatch COMMAND guest_fnmatch_tests) + if(WIN32) + set_tests_properties(guest_fnmatch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_resolver_tests ${LIBS_INCLUDE_DIR}/tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit libSceNet libc) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 8d405a75d..0c3b4b758 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -5,9 +5,156 @@ #include #include #include +#include + +namespace { + +constexpr int FnmNomatch = 1; +constexpr int FnmNoescape = 0x01; +constexpr int FnmPathname = 0x02; +constexpr int FnmPeriod = 0x04; +constexpr int FnmLeadingDir = 0x08; +constexpr int FnmCasefold = 0x10; + +enum class RangeResult { Match, NoMatch, Error }; + +unsigned char FoldCase(unsigned char c, int flags) { + return (flags & FnmCasefold) && c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; +} + +RangeResult MatchRange(const char* pattern, unsigned char test, int flags, const char** next) { + const bool negate = *pattern == '!' || *pattern == '^'; + if (negate) ++pattern; + test = FoldCase(test, flags); + bool ok = false; + const char* const origin = pattern; + for (;;) { + if (*pattern == ']' && pattern > origin) { + ++pattern; + break; + } + if (*pattern == '\0') return RangeResult::Error; + if (*pattern == '/' && (flags & FnmPathname)) return RangeResult::NoMatch; + if (*pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char low = FoldCase(static_cast(*pattern), flags); + if (*pattern != '\0') ++pattern; + if (*pattern == '-' && pattern[1] != '\0' && pattern[1] != ']') { + if (*++pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char high = static_cast(*pattern); + if (high == '\0') return RangeResult::Error; + ++pattern; + if (low <= test && test <= FoldCase(high, flags)) ok = true; + } else if (low == test) { + ok = true; + } + } + *next = pattern; + return ok == negate ? RangeResult::NoMatch : RangeResult::Match; +} + +int MatchPattern(const char* pattern, const char* string, int flags) { + const char* const start = string; + const char* backtrackPattern = nullptr; + const char* backtrackString = nullptr; + const auto leadingPeriod = [&](const char* at) { + return *at == '.' && (flags & FnmPeriod) && (at == start || ((flags & FnmPathname) && at[-1] == '/')); + }; + for (;;) { + const unsigned char pc = static_cast(*pattern); + if (pc != '\0') ++pattern; + const unsigned char sc = static_cast(*string); + bool mismatch = false; + bool literal = false; + unsigned char expected = pc; + switch (pc) { + case '\0': + if ((flags & FnmLeadingDir) && sc == '/') return 0; + if (sc == '\0') return 0; + mismatch = true; + break; + case '?': + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + ++string; + break; + case '*': { + char c = *pattern; + while (c == '*') c = *++pattern; + if (leadingPeriod(string)) { + mismatch = true; + break; + } + if (c == '\0') { + if (flags & FnmPathname) return (flags & FnmLeadingDir) || std::strchr(string, '/') == nullptr ? 0 : FnmNomatch; + return 0; + } + if (c == '/' && (flags & FnmPathname)) { + string = std::strchr(string, '/'); + if (string == nullptr) return FnmNomatch; + break; + } + backtrackPattern = pattern; + backtrackString = string; + break; + } + case '[': { + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + const char* next = nullptr; + const RangeResult result = MatchRange(pattern, sc, flags, &next); + if (result == RangeResult::Error) { + literal = true; + } else if (result == RangeResult::Match) { + pattern = next; + ++string; + } else { + mismatch = true; + } + break; + } + case '\\': + if (!(flags & FnmNoescape)) { + expected = static_cast(*pattern); + if (expected == '\0') return FnmNomatch; + ++pattern; + } + literal = true; + break; + default: + literal = true; + break; + } + if (literal) { + if (sc != '\0') ++string; + if (FoldCase(expected, flags) != FoldCase(sc, flags)) mismatch = true; + } + if (mismatch) { + if (backtrackPattern == nullptr) return FnmNomatch; + const unsigned char bc = static_cast(*backtrackString); + if (bc == '\0') return FnmNomatch; + if (bc == '/' && (flags & FnmPathname)) return FnmNomatch; + ++backtrackString; + pattern = backtrackPattern; + string = backtrackString; + } + } +} + +} extern "C" { +int APS5_VABI fnmatch_nid_postfix(const char* pattern, const char* string, int flags) { + if (pattern == nullptr || string == nullptr) throw std::invalid_argument("fnmatch: null pattern or string"); + return MatchPattern(pattern, string, flags); +} + char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { if (*needle == '\0') return const_cast(text); for (; *text != '\0'; ++text) { diff --git a/core/libs/tests/GuestFnmatch.cpp b/core/libs/tests/GuestFnmatch.cpp new file mode 100644 index 000000000..63b25c270 --- /dev/null +++ b/core/libs/tests/GuestFnmatch.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" int APS5_VABI fnmatch_nid_postfix(const char*, const char*, int); +struct Case { + const char* pattern; + const char* string; + int flags; + int result; +}; +int main() { + static const Case cases[] = { + {"*.txt", "notes.txt", 0x00, 0}, + {"*.txt", "notes.TXT", 0x00, 1}, + {"*.txt", "notes.TXT", 0x10, 0}, + {"a?c", "abc", 0x00, 0}, + {"a?c", "ac", 0x00, 1}, + {"a[bc]d", "acd", 0x00, 0}, + {"a[!bc]d", "acd", 0x00, 1}, + {"a[^bc]d", "aed", 0x00, 0}, + {"a[a-c]d", "abd", 0x00, 0}, + {"a[a-c]d", "add", 0x00, 1}, + {"a[]]d", "a]d", 0x00, 0}, + {"a[a-]d", "a-d", 0x00, 0}, + {"[A-C]x", "bx", 0x10, 0}, + {"*", "dir/file", 0x00, 0}, + {"*", "dir/file", 0x02, 1}, + {"dir/*", "dir/file", 0x02, 0}, + {"*/file", "dir/file", 0x02, 0}, + {"dir?file", "dir/file", 0x02, 1}, + {"dir[/]file", "dir/file", 0x02, 1}, + {"*", ".hidden", 0x04, 1}, + {".*", ".hidden", 0x04, 0}, + {"?hidden", ".hidden", 0x04, 1}, + {"dir/*", "dir/.hidden", 0x06, 1}, + {"dir/*", "dir/.hidden", 0x04, 0}, + {"dir/.*", "dir/.hidden", 0x06, 0}, + {"dir", "dir/sub/file", 0x08, 0}, + {"dir/*", "dir/sub/file", 0x0a, 0}, + {"dir", "dirx", 0x08, 1}, + {"\\*", "*", 0x00, 0}, + {"\\*", "x", 0x00, 1}, + {"\\*", "\\*", 0x01, 0}, + {"a\\", "a", 0x00, 1}, + {"a\\", "a\\", 0x01, 0}, + {"a*b*c", "axxbyyc", 0x00, 0}, + {"a*b*c", "axxbyy", 0x00, 1}, + {"*a*", "bbb", 0x00, 1}, + {"**a", "ba", 0x00, 0}, + {"", "", 0x00, 0}, + {"", "x", 0x00, 1}, + {"x", "", 0x00, 1}, + {"[", "[", 0x00, 0}, + {"[a", "[a", 0x00, 0}, + {"a[", "a[", 0x00, 0}, + {"[!]x", "x", 0x00, 1}, + {"*/", "dir/", 0x02, 0}, + {"*/", "dir", 0x02, 1}, + }; + for (const Case& entry : cases) { + if (fnmatch_nid_postfix(entry.pattern, entry.string, entry.flags) != entry.result) { + std::fprintf(stderr, "fnmatch(\"%s\", \"%s\", 0x%x) != %d\n", entry.pattern, entry.string, entry.flags, entry.result); + std::abort(); + } + } + bool threw = false; + try { + fnmatch_nid_postfix(nullptr, "x", 0); + } catch (const std::invalid_argument&) { + threw = true; + } + if (!threw) std::abort(); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..4155c8080 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -350,6 +350,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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 from the same thread-local host `std::random_device` as `arc4random_buf`, 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. Multibyte characters and locale collation are not modelled. A null pattern or string throws - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, 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. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` 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 cb9932007afeeed325d18c5921e1a6ee924c4a15 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:53:59 +0200 Subject: [PATCH 028/277] fix(relinker): ignore no-op executable relocations --- core/relinker/CMakeLists.txt | 2 + .../src/pipeline/RelinkerPipeline.cpp | 3 + .../relinker/tests/test_none_relocations.py | 93 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 core/relinker/relinker/tests/test_none_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..6375cdd47 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -245,6 +245,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) + add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 5b28eb708..26a2320b5 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -231,6 +231,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); + if (relType == R_X86_64_NONE) + continue; + if (relType == 8) { if (symIdx != 0) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py new file mode 100644 index 000000000..5a0d06fc3 --- /dev/null +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -0,0 +1,93 @@ +import json +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_dynamic_segment import fixture +from test_linux_dynamic_strings import dynamic_strings, file_offset +from test_linux_load_alignment import loads + + +def image_with_relocations(relocations, sce, imported): + image = fixture() + struct.pack_into(" Date: Sun, 11 Oct 2026 01:17:33 +0200 Subject: [PATCH 029/277] perf(agc-driver): prepare shader ABI variants before taking the preparation lock A shader preparation transaction took the global writers lock when it was constructed and kept it to its end. ResolveShaderAbi, reached from sceAgcCreateInterpolantMapping and sceAgcLinkShaders, compiled its variants inside such a transaction, so every game thread creating or linking shaders waited behind one compile: Uncharted's stalled frames spent about half a second each in shader registration. The root transaction now takes the lock on its first Read, Edit or Hold. ResolveShaderAbi prepares the variants before reading the prepared state when its ABI was not committed yet. Registry publication holds the lock as before: PublishRegisteredShader holds it, and RegisterShader holds it before the driver mutex, so the lock order is unchanged. Commit takes no lock: a transaction with changes already holds it. --- .../Execution/include/ShaderPreparation.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 25 +++++++++++++++++-- .../tests/execution/PreparedPublication.cpp | 24 ++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp index 57ce88164..98fd3cbad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp @@ -16,6 +16,7 @@ public: ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete; PreparedShaderState& Edit(const ShaderSnapshot& snapshot); const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const; + void Hold(); void Commit(); private: 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 9b46369ce..f24ab25a1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -130,6 +130,12 @@ void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock key) { + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); }); +} + } bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) { @@ -157,9 +163,13 @@ struct ShaderPreparationTransaction::State { }; static std::mutex writers; static thread_local State* active; - std::unique_lock lock{writers}; + std::unique_lock lock{writers, std::defer_lock}; std::map changes; bool failed = false; + + void Hold() { + if (!lock.owns_lock()) lock.lock(); + } }; std::mutex ShaderPreparationTransaction::State::writers; @@ -180,6 +190,7 @@ ShaderPreparationTransaction::~ShaderPreparationTransaction() { PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) { require(!committed, "shader preparation transaction is already committed"); + root->Hold(); auto& changes = root->changes; const auto found = changes.find(snapshot.prepared.get()); if (found != changes.end()) { @@ -193,6 +204,7 @@ PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& sn const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const { require(!committed, "shader preparation transaction is already committed"); + root->Hold(); auto& changes = root->changes; const auto found = changes.find(snapshot.prepared.get()); if (found != changes.end()) return found->second.prepared; @@ -201,6 +213,11 @@ const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapsh return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; } +void ShaderPreparationTransaction::Hold() { + require(!committed, "shader preparation transaction is already committed"); + root->Hold(); +} + void ShaderPreparationTransaction::Commit() { require(!committed && !root->failed, "shader preparation transaction was aborted"); if (state != nullptr) { @@ -294,6 +311,7 @@ std::shared_ptr RegisteredProgram(const ShaderRegistry& re void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot) { ShaderPreparationTransaction transaction; + transaction.Hold(); auto published = snapshot; if (registry != nullptr) { const auto found = registry->find(snapshot->codeAddress); @@ -935,12 +953,14 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + const bool committedAbi = CommittedAbi(*snapshot, key); + auto entries = committedAbi ? std::vector{} : PrepareRegistered(*snapshot, *localDevice, state, false); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - auto entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; }); @@ -1094,6 +1114,7 @@ void Driver::RegisterShader(const Shader* shader) { snapshot.header.resize(fields.header_size); std::memcpy(snapshot.header.data(), static_cast(shader), fields.header_size); + transaction.Hold(); { std::lock_guard lock(mutex); rethrowFailure(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index b6abe53ff..d96b7a9ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace { @@ -277,6 +278,28 @@ void Registration(AgcDriver::VulkanDevice& device) { Require(registry->at(0x10000).back() == metadata, "changed shader metadata was treated as identical"); } +void TransactionLock() { + ShaderSnapshot shader{}; + const auto publish = [&] { + ShaderPreparationTransaction other; + other.Edit(shader).rectangleRequested = true; + other.Commit(); + }; + std::optional open(std::in_place); + auto unread = std::async(std::launch::async, publish); + const bool unblocked = unread.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + if (!unblocked) open.reset(); + unread.get(); + Require(unblocked, "an open transaction that read nothing blocked another one"); + static_cast(open->Read(shader)); + auto waiting = std::async(std::launch::async, publish); + const bool excluded = waiting.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open->Commit(); + open.reset(); + waiting.get(); + Require(excluded, "a transaction ran beside one that had read prepared state"); +} + } int main() { @@ -284,6 +307,7 @@ int main() { Transactions(); HeaderSelection(); FusedHullHeaderSelection(); + TransactionLock(); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Registration(*device); From 82c04e4027ca7eeeb45e7db7ddc7646399d49da0 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 08:47:28 -0400 Subject: [PATCH 030/277] fix(shader): keep sRGB comparisons in the emulated runtime mode (cherry picked from commit 5377951c552f2313c8f7a52b57bd7a54c5f7fb7f) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 +++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 19 ++++++++++ .../tests/execution/EmulatedColorCompare.cpp | 36 ++++++++++++------- .../tests/execution/ImageSrgbLoad.cpp | 11 ++++++ .../Optimization/src/ResourceMaterializer.cpp | 4 +-- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..6f112764f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1014,6 +1014,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) + set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_emulated_color_compare agc_driver_emulated_color_compare_srgb_decode PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f..86779fda9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2912,6 +2912,24 @@ void verifyCubeDepthCompareMode() { require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); } +void verifySrgbDepthCompareModes() { + using namespace ShaderRecompiler; + for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.depthCompare = true; + image.srgbDecodeCompatible = false; + image.srgbDecodeFormats = decodeFormats; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + for (const auto format : {128u, 129u, 130u}) { + const DescriptorValue descriptor{{0x100u, format << 20u, 0u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(!modes[selected].srgbDecode && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "sRGB color comparison did not select its emulated runtime mode independently of decode support"); + } + } +} + int main(int argc, char** argv) { try { using namespace ShaderRecompiler; @@ -2958,6 +2976,7 @@ int main(int argc, char** argv) { verifyFunctionLdsBound(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); + verifySrgbDepthCompareModes(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 8a4da6082..e53b5dc51 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -236,10 +237,11 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false) { +std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false, std::uint64_t expectedVariant = 0) { Output.fill(-1.0f); const std::span code = offsets ? std::span(OffsetCode) : array ? std::span(ArrayCode) : std::span(Code); const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); + if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); @@ -251,6 +253,7 @@ void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* na const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f; Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); } + return result.variantId; } constexpr std::uint32_t Select(std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint32_t w) { @@ -306,9 +309,10 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } -void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { +void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { Output.fill(-1.0f); - const auto result = Compile(device, format, sampler); + const auto result = Compile(device, format, sampler, Code, useCache); + if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); @@ -333,6 +337,8 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const auto* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::string_view(forced) == "1") Require((device->Target().srgbDecodeFormats & 3u) == 3u, "forced R8 and RG8 sRGB shader decoding is unavailable"); FillTexels(); FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); @@ -344,16 +350,22 @@ int main() { FillTexels(); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); - for (std::uint32_t function = 0; function < 8u; ++function) { - const auto point = "point 8_8_8_8 srgb function " + std::to_string(function); - const auto bilinear = "bilinear 8_8_8_8 srgb function " + std::to_string(function); - RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, point.c_str()); - RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, bilinear.c_str()); + for (const bool useCache : {false, true}) { + const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u; + if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity"); + for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) { + for (std::uint32_t function = 0; function < 8u; ++function) { + for (const auto filter : {FilterPoint, FilterBilinear}) { + const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear"); + RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); + } + } + } + if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant); + } + for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) { + Reject(*device, format, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); } - RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterPoint}, "point 8 srgb"); - RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb"); - RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterPoint}, "point 8_8 srgb"); - RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb"); Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index a1f35fc45..ec759a3f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -66,12 +66,21 @@ constexpr std::array SamplerCode(std::uint32_t word0) { }; } +constexpr std::array MixedSamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, 0x7e2c0280u, 0xf0bc0108u, 0x00610a14u, 0xbf8c3f70u, + 0xe0701000u, 0x80000a03u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0701004u, 0x80000a03u, 0xbf810000u, + }; +} + alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu); alignas(256) constexpr auto LoadXy = LoadCode(0x3u); alignas(256) constexpr auto StoreX = StoreCode(0x1u); alignas(256) constexpr auto StoreXy = StoreCode(0x3u); alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); +alignas(256) constexpr auto MixedSampleLz = MixedSamplerCode(0xf09c0f08u); +alignas(256) constexpr auto MixedGather4Lz = MixedSamplerCode(0xf11c0108u); class GuestBlock { public: @@ -235,6 +244,8 @@ int main() { CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores"); RequireRefused(*device, SampleLz, "image_sample_lz"); RequireRefused(*device, Gather4Lz, "image_gather4_lz"); + RequireRefused(*device, MixedSampleLz, "image_sample_c_lz followed by image_sample_lz"); + RequireRefused(*device, MixedGather4Lz, "image_sample_c_lz followed by image_gather4_lz"); std::puts("image sRGB load 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 dd0aaded9..2822f4124 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -191,8 +191,8 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) throw std::runtime_error("selects a channel the converted image format does not have"); } } - decoded.srgbDecode = !storage && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; - if (decoded.srgbDecode && !base.srgbDecodeCompatible && !base.depthCompare) { + decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; + if (decoded.srgbDecode && !base.srgbDecodeCompatible) { throw std::runtime_error("samples or gathers an sRGB image the device cannot sample, which is not implemented"); } if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { From 79c8d3690133a35102eb3ac0095a99153dcf8456 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sun, 11 Oct 2026 14:58:57 +0200 Subject: [PATCH 031/277] fix(agc): reject constant color factors and memory-writing stages in split SWAP_ALT blends Review follow-up to the 8_8 SWAP_ALT blending. The second channel of such a target holds the guest's alpha. Vulkan applies the green blend constant to it for CONSTANT_COLOR and ONE_MINUS_CONSTANT_COLOR, where the guest's alpha equation uses the alpha constant. Decoding now throws for these factors on a SWAP_ALT 8_8 target, unless the equation is MIN or MAX, which ignore factors. The second pass of a split draw runs every stage again, not only the pixel shader, so a vertex or geometry stage that writes memory would write twice. The memory-write check (now ShadersWriteMemory) covers all the draw's stages. --- .../Graphics/include/Pipeline.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 32 +++++++++---------- .../libSceAgcDriver/Graphics/src/State.cpp | 2 ++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 30 +++++++++++++++++ 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index c4fa93353..5258bc026 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -107,6 +107,7 @@ void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); +bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d47dcb3da..43dbbbe4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -115,23 +115,10 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -namespace { - -VkShaderStageFlags pushStagesOf(const Context& context) { - return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); -} - -template -void appendKey(std::vector& key, const TValue& value) { - static_assert(std::is_trivially_copyable_v); - const auto bytes = std::as_bytes(std::span(&value, 1)); - key.insert(key.end(), bytes.begin(), bytes.end()); -} - -bool fragmentWritesMemory(std::span shaders) { +bool ShadersWriteMemory(std::span shaders) { const auto any = [](const std::vector& written, std::uint32_t count) { return written.size() < count || std::ranges::any_of(written, [](bool element) { return element; }); }; for (const auto& shader : shaders) { - if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + if (shader.program == nullptr) continue; for (const auto& binding : shader.program->bindings) { switch (binding.role) { case ShaderRecompiler::DescriptorRole::GuestBuffers: @@ -154,6 +141,19 @@ bool fragmentWritesMemory(std::span shaders) { return false; } +namespace { + +VkShaderStageFlags pushStagesOf(const Context& context) { + return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); +} + +template +void appendKey(std::vector& key, const TValue& value) { + static_assert(std::is_trivially_copyable_v); + const auto bytes = std::as_bytes(std::span(&value, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); +} + bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; @@ -379,7 +379,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (alternateAlpha) { const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); }; Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented"); - Require(!fragmentWritesMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a pixel shader that writes memory is not implemented"); + Require(!ShadersWriteMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw whose shaders write memory is not implemented"); for (std::size_t index = 0; index < secondChannels.size(); ++index) { const bool split = index < state.colors.size() && state.colors[index].componentMapping == 0xecu && state.blends[index].blendEnable; if (!split) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cf0d6b1ea..f1eec43a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -771,6 +771,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (mapping == 0xecu) { const auto destinationAlpha = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; }; if (destinationAlpha(state.srcColorBlendFactor) || destinationAlpha(state.dstColorBlendFactor) || destinationAlpha(state.srcAlphaBlendFactor) || destinationAlpha(state.dstAlphaBlendFactor)) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a factor of the destination alpha, which the target keeps in its second channel, is not implemented"); + const auto weighted = [](VkBlendOp operation) { return operation != VK_BLEND_OP_MIN && operation != VK_BLEND_OP_MAX; }; + if ((weighted(state.colorBlendOp) && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) || (weighted(state.alphaBlendOp) && (constantColor(state.srcAlphaBlendFactor) || constantColor(state.dstAlphaBlendFactor)))) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a constant color factor, which Vulkan applies to the second channel with the green blend constant instead of the alpha one, is not implemented"); } } if (color.packing != ShaderRecompiler::ColorExportPacking::None) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..4fa978039 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -878,6 +878,29 @@ void TuningFieldTests() { } } +void ShadersWriteMemoryTests() { + const auto binding = [](ShaderRecompiler::DescriptorKind kind, ShaderRecompiler::DescriptorRole role, std::vector written) { + ShaderRecompiler::DescriptorBinding result{}; + result.kind = kind; + result.role = role; + result.count = 1; + if (role == ShaderRecompiler::DescriptorRole::GuestImages) result.imageWritten = std::move(written); + else result.bufferWritten = std::move(written); + return result; + }; + ShaderRecompiler::RecompileResult reads; + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {false})); + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, {false})); + ShaderRecompiler::RecompileResult stores; + stores.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {true})); + using Stage = ShaderRecompiler::ShaderStage; + const auto writes = [](std::initializer_list shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector(shaders)); }; + Require(!writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &reads, 0}}), "shaders that only read memory were reported as writing it"); + Require(writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &stores, 0}}), "a pixel shader that stores to a buffer was not reported"); + Require(writes({{Stage::Vertex, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a vertex shader that stores to a buffer was not reported"); + Require(writes({{Stage::Geometry, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a geometry shader that stores to a buffer was not reported"); +} + void ReversedComponentOrderTests() { { auto queue = makeState(); @@ -891,6 +914,12 @@ void ReversedComponentOrderTests() { Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw"); queue.context[0x1e0] = 0x60000001u; Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel"); + queue.context[0x1e0] = (1u << 30u) | 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 1u | (1u << 16u) | (14u << 24u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (2u << 5u) | 13u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorBlendOp == VK_BLEND_OP_MIN, "a constant color factor of a MIN blend into an 8_8 SWAP_ALT target was rejected"); } for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) { auto queue = makeState(); @@ -4305,6 +4334,7 @@ int main(int argc, char** argv) { OpaqueDestinationAlphaTests(); CompactedExportTests(); ReversedComponentOrderTests(); + ShadersWriteMemoryTests(); DualSourceBlendTests(); metadataPassTests(); cmaskTests(); From e9736282f7dc6a7b63d4e2e453d26789670a20a0 Mon Sep 17 00:00:00 2001 From: ducloc080202 Date: Sun, 11 Oct 2026 19:59:13 +0700 Subject: [PATCH 032/277] fix(libc): name the throw site in unhandled exception reports --- core/libs/prx/libc/src/exception/Exports.cpp | 2 +- core/libs/prx/libc/src/exception/Runtime.cpp | 8 +- core/libs/tests/WindowsExceptions.cpp | 77 +++++++++++++++++++- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 2bf7ef211..dbc0c7acb 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -52,7 +52,7 @@ void NativeFreeException(void* object) asm("__cxa_free_exception"); void NativeFreeException(void* object) { __cxa_free_exception_nid_postfix(object); } [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) asm("__cxa_throw"); [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) { - __cxa_throw_nid_postfix(object, type, destructor); + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); } void* NativeBeginCatch(void* exception) asm("__cxa_begin_catch"); void* NativeBeginCatch(void* exception) { return __cxa_begin_catch_nid_postfix(exception); } diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index b9e26d77f..7b18999b7 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -229,7 +229,7 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { std::free(allocation); } -[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { +[[noreturn]] static void ThrowFromSite(void* object, std::type_info* type, void (*destructor)(void*), void* throwSite) { using namespace LibcException; auto* header = FromObject(object); header->type = type; @@ -240,11 +240,15 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { header->unwind.exception_cleanup = Cleanup; ++globals.uncaught; _Unwind_RaiseException_nid_postfix(&header->unwind); - RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", __builtin_return_address(0)); + RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", throwSite); __cxa_begin_catch_nid_postfix(&header->unwind); InvokeTerminate(header->terminate); } +[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); +} + LibcException::Header* __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { using namespace LibcException; auto* header = FromObject(object); diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 3b5bc6116..63f8cb1d3 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -1,14 +1,18 @@ #include #include #include +#include #include #include +#include +#include #include #include #include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" extern "C" { @@ -55,6 +59,72 @@ static void TestTypeInfoVtables() { extern "C" void NotImplemented_nid_no_patch(const char*); +static void RequireHandle(bool value) { + if (!value) throw std::runtime_error("child process setup failed"); +} + +[[gnu::noinline, noreturn]] static void ThrowUncaughtProbe() { + throw std::runtime_error("unhandled site probe"); +} + +static int RunThrowChild() { + std::fprintf(stderr, "probe %p\n", reinterpret_cast(&ThrowUncaughtProbe)); + std::fflush(stderr); + ThrowUncaughtProbe(); + return 100; +} + +static void testUnhandledThrowSite() { + SECURITY_ATTRIBUTES attributes{}; + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = TRUE; + HANDLE readPipe = nullptr; + HANDLE writePipe = nullptr; + RequireHandle(CreatePipe(&readPipe, &writePipe, &attributes, 0) != 0); + RequireHandle(SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0) != 0); + std::array image{}; + RequireHandle(GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())) != 0); + const wchar_t* argument = L" throw-uncaught"; + std::array command{}; + const std::size_t imageLength = std::wcslen(image.data()); + const std::size_t argumentLength = std::wcslen(argument); + RequireHandle(imageLength + argumentLength + 1 < command.size()); + std::memcpy(command.data(), image.data(), imageLength * sizeof(wchar_t)); + std::memcpy(command.data() + imageLength, argument, (argumentLength + 1) * sizeof(wchar_t)); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = writePipe; + startup.hStdError = writePipe; + PROCESS_INFORMATION child{}; + RequireHandle(CreateProcessW(image.data(), command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child) != 0); + CloseHandle(writePipe); + CloseHandle(child.hThread); + std::array output{}; + std::size_t outputLength = 0; + DWORD chunk = 0; + while (outputLength < output.size() && ReadFile(readPipe, output.data() + outputLength, static_cast(output.size() - outputLength), &chunk, nullptr) != 0 && chunk != 0) { + outputLength += chunk; + } + CloseHandle(readPipe); + RequireHandle(WaitForSingleObject(child.hProcess, 20000) == WAIT_OBJECT_0); + DWORD exitCode = 0; + RequireHandle(GetExitCodeProcess(child.hProcess, &exitCode) != 0); + CloseHandle(child.hProcess); + output[outputLength < output.size() ? outputLength : output.size() - 1] = 0; + RequireHandle(exitCode == 0xC0000409); + const char* probeMarker = std::strstr(output.data(), "probe "); + RequireHandle(probeMarker != nullptr); + const auto probe = static_cast(std::strtoull(probeMarker + 6, nullptr, 16)); + RequireHandle(probe != 0); + const char* marker = std::strstr(output.data(), "thrown from "); + RequireHandle(marker != nullptr); + const auto logged = static_cast(std::strtoull(marker + 13, nullptr, 16)); + RequireHandle(logged != 0); + if (logged <= probe || logged - probe >= 0x1000) throw std::runtime_error("unhandled exception named the wrapper instead of the throw site"); +} + static int destroyed; struct Guard { ~Guard() { ++destroyed; } }; static void ThrowNested() { @@ -178,8 +248,13 @@ static void testUnwindThroughRuntimeModules() { } } -int main() { +int main(int argc, char** argv) { + if (argc > 1) { + if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); + return 100; + } TestTypeInfoVtables(); + testUnhandledThrowSite(); testExceptionPointer(); testFutureException(); testMadeExceptionPointer(); From f22b9fa02ae42070dba52b5bce908c81004162f8 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:19:27 +0200 Subject: [PATCH 033/277] fix(shader): round v_fma_mixlo_f16 and v_fma_mixhi_f16 in every f16 rounding mode The f16 forms of v_fma_mix threw for any FLOAT_MODE with an f16 rounding other than to nearest even. ps5-opengl's shaders run with FLOAT_MODE 0xcc, f16 rounding toward zero with f16 denormals kept, so PPSA99005 stopped at its first draw (#2631). The exact product-sum, already computed in f64 with round to odd, is now rounded once in the mode's direction: scaled by the f16 step at its exponent, truncated, floored or ceiled, and scaled back. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the direction. On RDNA2 (gfx1036) -1.5 * 2^-24 + 2^-126 gives 0x8002 with round toward -infinity, and this gives the same. Flushed f16 denormals still throw. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/MixPrecision.cpp | 46 ++++++++++++++++++- core/libs/tests/VectorFusedMultiplyAdd.cpp | 8 +++- core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterFloat64.cpp | 34 ++++++++++++-- .../Translation/src/FloatInstructions.cpp | 7 +-- docs/dev/TechnicalDebt.md | 2 +- 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp index d8c1506e3..6c953b8da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,24 @@ constexpr std::array RoundingRows{{ {0x00003c01u, 0x00004200u, 0x00800000u, 3u, 0x4202u}, }}; +struct DirectedRow { + std::uint32_t a, b, c; + std::array expected; +}; + +constexpr std::array DirectedRows{{ + {0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}}, + {0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}}, + {0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}}, + {0xbf800000u, 0x3f801400u, 0x00000000u, {0xbc01u, 0xbc00u, 0xbc01u, 0xbc00u}}, + {0x477ff000u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0xc77ff000u, 0x3f800000u, 0x00000000u, {0xfc00u, 0xfbffu, 0xfc00u, 0xfbffu}}, + {0x49742400u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}}, + {0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}}, + {0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}}, +}}; + float Quarter(std::uint32_t index) { return static_cast(static_cast(index % 17u) - 8) / 4.0f; } @@ -176,7 +195,7 @@ void FillRoundingInput() { } } -void Run(AgcDriver::VulkanDevice& device, std::span code) { +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode = std::nullopt) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); @@ -192,6 +211,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span code) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = floatMode; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -235,6 +255,25 @@ void CheckRounding() { } } +void FillDirectedInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + std::copy_n(std::array{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs); + } +} + +void CheckDirected(std::uint32_t rounding) { + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + const auto* out = &Output[tid * Results]; + const std::uint32_t expected = row.expected[rounding]; + const auto where = "mix f16 rounding " + std::to_string(rounding) + ": thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ")"; + Require(out[0] == (0xabcd0000u | expected), where + " v_fma_mixlo_f16 is " + Hex(out[0]) + ", expected " + Hex(0xabcd0000u | expected)); + Require(out[1] == ((expected << 16u) | 0xabcdu), where + " v_fma_mixhi_f16 is " + Hex(out[1]) + ", expected " + Hex((expected << 16u) | 0xabcdu)); + } +} + } int main() { @@ -252,6 +291,11 @@ int main() { FillRoundingInput(); Run(*device, RoundingCode); CheckRounding(); + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + FillDirectedInput(); + Run(*device, RoundingCode, ShaderRecompiler::ShaderFloatMode{0xc0u | (rounding << 2u), true, false, false}); + CheckDirected(rounding); + } std::puts("mix precision tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/VectorFusedMultiplyAdd.cpp b/core/libs/tests/VectorFusedMultiplyAdd.cpp index f17e7f083..3f2926517 100644 --- a/core/libs/tests/VectorFusedMultiplyAdd.cpp +++ b/core/libs/tests/VectorFusedMultiplyAdd.cpp @@ -105,7 +105,10 @@ void CheckMixHalf(const std::string& name, const std::array& w std::uint32_t exact = 0u; for (const auto* value : block.Instructions()) { Require(value->Opcode() != IrOpcode::FPFma32 && value->Opcode() != IrOpcode::FPMad32, name + " rounds to f32 before rounding to f16"); - if (value->Opcode() == IrOpcode::FPInterpolateF16) ++exact; + if (value->Opcode() != IrOpcode::FPInterpolateF16) continue; + ++exact; + const std::uint32_t rounding = (value->Argument(3)->ImmediateU32() >> InterpolationRound16Shift) & 3u; + Require(rounding == ((floatMode >> 2u) & 3u), name + " rounds in f16 mode " + std::to_string(rounding)); } Require(exact == 1u, name + " emits " + std::to_string(exact) + " product-sums rounded once to f16"); } @@ -146,7 +149,8 @@ int main() { CheckMixHalf("v_fma_mixlo_f16 op_sel_hi:[1,1,1]", {0xcc214005u, 0x1c220f06u}, RdnaOpcode::VMadMixloF16, 0xc0u); CheckMixHalf("v_fma_mixlo_f16 with f32 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xf3u); RejectMixHalf("v_fma_mixlo_f16 with f16 denormals flushed", {0xcc210005u, 0x04220f06u}, 0x30u); - RejectMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, 0xf8u); + CheckMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, RdnaOpcode::VMadMixhiF16, 0xf8u); + CheckMixHalf("v_fma_mixlo_f16 with f16 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xccu); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index deca904d9..85f9689e8 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -176,6 +176,7 @@ struct ShaderFloatMode { inline constexpr std::uint32_t InterpolationQuiet = 1u; inline constexpr std::uint32_t InterpolationFlush32 = 2u; inline constexpr std::uint32_t InterpolationFlush16 = 4u; +inline constexpr std::uint32_t InterpolationRound16Shift = 3u; struct GuestContext { std::uint32_t waveSize; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index b9a4a629f..1ecf00b14 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -223,6 +223,23 @@ std::uint32_t RoundToPrecision(SpirvEmitterState& state, std::uint32_t value, st return ToF64(state, Join(state, {rounded.low, Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseAnd, u32, rounded.high, ConstantU32(state, 0x7fffffffu)), sign)})); } +std::uint32_t RoundToPrecisionDirected(SpirvEmitterState& state, std::uint32_t value, std::uint32_t fractionBits, std::uint32_t smallestStep, std::uint32_t rounding) { + const auto bits = Split(state, FromF64(state, value)); + const auto exponent = Unary(state, spv::OpBitcast, TypeI32(state), ExponentField(state, bits.high)); + const auto step = Glsl(state, TypeI32(state), GLSLstd450SMax, Binary(state, spv::OpISub, TypeI32(state), exponent, ConstantI32(state, static_cast(fractionBits))), ConstantI32(state, static_cast(smallestStep))); + const auto unbiased = Binary(state, spv::OpISub, TypeI32(state), step, ConstantI32(state, 1023)); + const auto scaled = Exact(state, spv::OpFMul, value, PowerOfTwo(state, Unary(state, spv::OpSNegate, TypeI32(state), unbiased))); + std::uint32_t integral = 0; + if (rounding == 3u) { + integral = Trunc(state, FromF64(state, scaled)); + } else if (rounding == 2u) { + integral = Floor(state, FromF64(state, scaled)); + } else { + integral = Unary(state, spv::OpFNegate, TypeF64(state), Floor(state, FromF64(state, Unary(state, spv::OpFNegate, TypeF64(state), scaled)))); + } + return Exact(state, spv::OpFMul, integral, PowerOfTwo(state, unbiased)); +} + std::uint32_t F32BitsOfRounded(SpirvEmitterState& state, std::uint32_t value) { const auto u32 = TypeU32(state); const auto bits = Split(state, FromF64(state, value)); @@ -265,12 +282,23 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); const auto product = Exact(state, spv::OpFMul, FromF32Exact(state, delta), FromF32Exact(state, coordinate)); const auto sum = AddRoundToOdd(state, product, FromF32Exact(state, base)); - auto rounded = halfResult ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecision(state, sum, 23u, 874u); + const std::uint32_t rounding16 = halfResult ? (mode >> InterpolationRound16Shift) & 3u : 0u; + auto rounded = halfResult ? (rounding16 == 0u ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecisionDirected(state, sum, 10u, 999u, rounding16)) : RoundToPrecision(state, sum, 23u, 874u); if (halfResult) { const auto bits = Split(state, FromF64(state, rounded)); const auto sign = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x80000000u)); - const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)), ConstantU32(state, 0x40f00000u)); - rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + const auto magnitude = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)); + if (rounding16 == 0u) { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40f00000u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + } else { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40effc00u)); + const auto negative = Binary(state, spv::OpINotEqual, TypeBool(state), sign, ConstantU32(state, 0u)); + auto limit = ConstantU32(state, 0x40effc00u); + if (rounding16 == 1u) limit = Select(state, u32, negative, ConstantU32(state, 0x40effc00u), ConstantU32(state, 0x40f00000u)); + if (rounding16 == 2u) limit = Select(state, u32, negative, ConstantU32(state, 0x40f00000u), ConstantU32(state, 0x40effc00u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, limit)}, bits))); + } } const auto finite = F32BitsOfRounded(state, rounded); const auto inputs = std::array{Unary(state, spv::OpBitcast, u32, delta), Unary(state, spv::OpBitcast, u32, coordinate), Unary(state, spv::OpBitcast, u32, base)}; diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 2ca7925c1..b1ef1e9e2 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -230,8 +230,8 @@ bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opc } bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { - if (mix && floatMode.has_value() && (floatMode->floatMode & 0xccu) != 0xc0u) { - throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept or rounding not to nearest even) is not implemented"); + if (mix && floatMode.has_value() && (floatMode->floatMode & 0xc0u) != 0xc0u) { + throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept) is not implemented"); } std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { @@ -243,7 +243,8 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op return true; } if (mix) { - writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(0u)})), {args[0], args[1], args[2]}); + const std::uint32_t rounding = floatMode.has_value() ? ((floatMode->floatMode >> 2u) & 3u) << InterpolationRound16Shift : 0u; + writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(rounding)})), {args[0], args[1], args[2]}); return true; } writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]})), {args[0], args[1], args[2]}); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..7ed0edc86 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -468,7 +468,7 @@ Throughout the project, every function at every stage either **does exactly what - The [dispatches that follow a compute queue's wait](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. Only f16 denormals kept with round to nearest even is translated; another f16 mode throws, as for packed f16. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction, which gives the measured 0x8001 and 0x8002 above; round toward +infinity and toward zero (`FLOAT_MODE` 0xcc, used by ps5-opengl) are not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. From bc517d6058e4cbb6c319285f8b695fd6c462f1d0 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:27:07 +0200 Subject: [PATCH 034/277] fix(shader): give an exact zero mix f16 sum -0 when rounding toward -infinity With f16 rounding toward -infinity, v_fma_mixlo_f16 and v_fma_mixhi_f16 gave +0 for an exact cancellation such as 1.0 * 1.0 + -1.0, because the f64 sum is computed to nearest. RDNA2 gives -0 there, as IEEE 754 does for an exact zero sum rounded toward -infinity, measured on gfx1036 in #2631. An exact zero sum now gives -0 in that mode unless both the product and the addend are +0. A nonzero result that rounds to zero keeps its sign, so a tiny positive sum still gives +0. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/execution/MixPrecision.cpp | 8 +++++++- .../SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp index 6c953b8da..f7d68a606 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -121,7 +121,7 @@ struct DirectedRow { std::array expected; }; -constexpr std::array DirectedRows{{ +constexpr std::array DirectedRows{{ {0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}}, {0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}}, {0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}}, @@ -132,6 +132,12 @@ constexpr std::array DirectedRows{{ {0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}}, {0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}}, {0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}}, + {0x3f800000u, 0x3f800000u, 0xbf800000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x40000000u, 0x3f800000u, 0xc0000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x00000000u, 0x3f800000u, 0x80000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x80000000u, 0x3f800000u, 0x80000000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u}}, + {0x33000000u, 0x3f000000u, 0x00000000u, {0x0000u, 0x0001u, 0x0000u, 0x0000u}}, + {0xb3000000u, 0x3f000000u, 0x00000000u, {0x8000u, 0x8000u, 0x8001u, 0x8000u}}, }}; float Quarter(std::uint32_t index) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 1ecf00b14..241522188 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -298,6 +298,15 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui if (rounding16 == 1u) limit = Select(state, u32, negative, ConstantU32(state, 0x40effc00u), ConstantU32(state, 0x40f00000u)); if (rounding16 == 2u) limit = Select(state, u32, negative, ConstantU32(state, 0x40f00000u), ConstantU32(state, 0x40effc00u)); rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, limit)}, bits))); + if (rounding16 == 2u) { + const auto boolean = TypeBool(state); + const auto zeroBits = [&](F64Bits value, std::uint32_t highMask) { + return Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, value.high, ConstantU32(state, highMask)), ConstantU32(state, 0u)), Binary(state, spv::OpIEqual, boolean, value.low, ConstantU32(state, 0u))); + }; + const auto positiveZeros = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, product)), 0xffffffffu), zeroBits(Split(state, FromF64(state, FromF32Exact(state, base))), 0xffffffffu)); + const auto negativeZero = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, sum)), 0x7fffffffu), Unary(state, spv::OpLogicalNot, boolean, positiveZeros)); + rounded = ToF64(state, Join(state, SelectBits(state, negativeZero, {ConstantU32(state, 0u), ConstantU32(state, 0x80000000u)}, Split(state, FromF64(state, rounded))))); + } } } const auto finite = F32BitsOfRounded(state, rounded); From 23dad48559f28779764d86abc74982c92e7a26df Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 11 Oct 2026 14:46:53 +0200 Subject: [PATCH 035/277] docs(debt): record the RDNA2 sweep for every f16 rounding mode of v_fma_mixlo/mixhi_f16 Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7ed0edc86..0fd80f472 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -468,7 +468,7 @@ Throughout the project, every function at every stage either **does exactly what - The [dispatches that follow a compute queue's wait](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction, which gives the measured 0x8001 and 0x8002 above; round toward +infinity and toward zero (`FLOAT_MODE` 0xcc, used by ps5-opengl) are not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. From 7a68e11ab4992bc4b9e1cbe99ea59717fc39308b Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:33:16 +0300 Subject: [PATCH 036/277] fix(libkernel): rewind Windows directory descriptors on lseek lseek on a directory descriptor returned 0 without moving the listing, so getdents after lseek(fd, 0, SEEK_SET) reported nothing. --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++ .../File/include/DirectoryDescriptor.hpp | 2 + .../File/src/DirectoryDescriptor.cpp | 56 +++++++++++--- core/libs/prx/libkernel/File/src/Open.cpp | 8 ++ core/libs/tests/GuestDirectoryRewind.cpp | 75 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/GuestDirectoryRewind.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..db2752fa4 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -908,6 +908,15 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_buffer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_directory_rewind_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryRewind.cpp) + target_include_directories(guest_directory_rewind_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_rewind_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_directory_rewind_tests) + add_test(NAME guest_directory_rewind COMMAND guest_directory_rewind_tests) + set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(NOT WIN32) add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 9efda2d7c..2031b6c4c 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP #define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#include #include #include @@ -11,6 +12,7 @@ namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); } diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 06b665940..9210e95a3 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -7,8 +7,12 @@ #ifdef _WIN32 +#include "prx/libc/include/General.hpp" + +#include #include #include +#include #include #include #include @@ -32,6 +36,18 @@ struct DirectoryState { std::mutex g_mutex; std::map g_directories; +void Load(DirectoryState& state) { + if (state.loaded) return; + state.loaded = true; + state.entries.emplace_back(".", GuestDirectoryType); + state.entries.emplace_back("..", GuestDirectoryType); + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } +} + } namespace File { @@ -56,21 +72,41 @@ void ForgetDirectoryDescriptor(int fd) { g_directories.erase(fd); } +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { + constexpr int SeekSet = 0; + constexpr int SeekCurrent = 1; + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return std::nullopt; + auto& state = found->second; + if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); + const auto cursor = static_cast(state.cursor); + if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { + errno = EINVAL; + return std::int64_t{-1}; + } + const std::int64_t target = whence == SeekSet ? offset : cursor + offset; + if (target < 0) { + errno = EINVAL; + return std::int64_t{-1}; + } + if (target == 0) { + state.entries.clear(); + state.loaded = false; + state.cursor = 0; + return std::int64_t{0}; + } + Load(state); + state.cursor = static_cast(target); + return target; +} + int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { std::lock_guard lock(g_mutex); const auto found = g_directories.find(fd); if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = found->second; - if (!state.loaded) { - state.loaded = true; - state.entries.emplace_back(".", GuestDirectoryType); - state.entries.emplace_back("..", GuestDirectoryType); - std::error_code error; - for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); - } - } + Load(state); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ca7cbe5f1..50ad1b34a 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -215,6 +215,14 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } +#ifdef _WIN32 + if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { + if (*directory < 0) { + throw std::runtime_error(std::string(__func__) + ": lseek failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + } + return *directory; + } +#endif std::int64_t result = NativeLseek(d, offset, whence); if (result < 0) return SceIoErrorFromErrno(errno); return result; diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp new file mode 100644 index 000000000..da36e6ad0 --- /dev/null +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory rewind check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::set ReadAll(int directory) { + std::set names; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0); + if (bytes == 0) return names; + for (int offset = 0; offset < bytes;) { + names.emplace(buffer.data() + offset + 8); + offset += static_cast(buffer[offset + 4]) | (static_cast(buffer[offset + 5]) << 8); + } + } +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-rewind-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream file(root / "a"); Require(static_cast(file)); } + { std::ofstream file(root / "b"); Require(static_cast(file)); } + + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + const std::set expected{".", "..", "a", "b"}; + Require(ReadAll(directory) == expected); + Require(ReadAll(directory).empty()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == expected); + + { std::ofstream file(root / "c"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + + std::array two{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const auto position = sceKernelLseek(directory, 0, 1); + Require(position > 0); + const auto rest = ReadAll(directory); + Require(sceKernelLseek(directory, position, 0) == position); + Require(ReadAll(directory) == rest); + Require(!rest.contains(".") && !rest.contains("..")); + + Require(sceKernelClose(directory) == 0); + std::filesystem::remove_all(root); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..81478df5a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,6 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries`; `lseek` on them takes `SEEK_SET` and `SEEK_CUR` offsets as entry indexes and `SEEK_END` throws. Offset 0 rewinds and takes a new snapshot, as FreeBSD `getdirentries(2)` allows. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From cb6dcbf8449f89ac15e5dd085ed9ed2556bff092 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:46:10 +0300 Subject: [PATCH 037/277] fix(libkernel): keep directory lseek off the shared lock and report offset overflow lseek on a non-directory descriptor no longer takes the directory table lock when no directory is open, and a listing is read under the descriptor's own lock. SEEK_CUR with offset 0 only reports the position, and an overflowing SEEK_CUR offset reports EOVERFLOW. --- .../File/src/DirectoryDescriptor.cpp | 47 ++++++++++------ core/libs/tests/GuestDirectoryRewind.cpp | 53 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 9210e95a3..3b9be91af 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -11,11 +11,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -25,8 +27,10 @@ namespace { constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; +constexpr int GuestEoverflow = 84; struct DirectoryState { + std::mutex mutex; std::filesystem::path path; std::vector> entries; std::size_t cursor = 0; @@ -34,7 +38,14 @@ struct DirectoryState { }; std::mutex g_mutex; -std::map g_directories; +std::map> g_directories; +std::atomic g_directoryCount{0}; + +std::shared_ptr Find(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + return found == g_directories.end() ? nullptr : found->second; +} void Load(DirectoryState& state) { if (state.loaded) return; @@ -56,33 +67,39 @@ int OpenDirectoryDescriptor(const std::filesystem::path& path) { const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); if (fd < 0) return -1; std::lock_guard lock(g_mutex); - g_directories[fd] = DirectoryState{path}; + const auto state = std::make_shared(); + state->path = path; + g_directories[fd] = state; + g_directoryCount = g_directories.size(); return fd; } std::optional DirectoryDescriptorPath(int fd) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - return found->second.path; + const auto state = Find(fd); + if (!state) return std::nullopt; + return state->path; } void ForgetDirectoryDescriptor(int fd) { + if (g_directoryCount == 0) return; std::lock_guard lock(g_mutex); g_directories.erase(fd); + g_directoryCount = g_directories.size(); } std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { constexpr int SeekSet = 0; constexpr int SeekCurrent = 1; - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - auto& state = found->second; + if (g_directoryCount == 0) return std::nullopt; + const auto found = Find(fd); + if (!found) return std::nullopt; + auto& state = *found; + std::lock_guard lock(state.mutex); if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); const auto cursor = static_cast(state.cursor); + if (whence == SeekCurrent && offset == 0) return cursor; if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { - errno = EINVAL; + errno = GuestEoverflow; return std::int64_t{-1}; } const std::int64_t target = whence == SeekSet ? offset : cursor + offset; @@ -102,10 +119,10 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, } int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; - auto& state = found->second; + const auto found = Find(fd); + if (!found) return SCE_KERNEL_ERROR_ENOTDIR; + auto& state = *found; + std::lock_guard lock(state.mutex); Load(state); std::size_t used = 0; while (state.cursor < state.entries.size()) { diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index da36e6ad0..cfb258e7e 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include extern "C" { @@ -26,6 +28,16 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static std::set ReadAll(int directory) { std::set names; std::array buffer{}; @@ -64,12 +76,51 @@ int main() { Require(sceKernelLseek(directory, 0, 0) == 0); Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); const auto position = sceKernelLseek(directory, 0, 1); - Require(position > 0); + Require(position == 2); + Require(sceKernelLseek(directory, 0, 1) == 2); const auto rest = ReadAll(directory); Require(sceKernelLseek(directory, position, 0) == position); Require(ReadAll(directory) == rest); Require(!rest.contains(".") && !rest.contains("..")); + Require(sceKernelLseek(directory, 100, 0) == 100); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 0); + Require(sceKernelLseek(directory, 0, 1) == 100); + Require(sceKernelLseek(directory, -98, 1) == 2); + Require(ReadAll(directory) == rest); + + Require(sceKernelLseek(directory, 3, 0) == 3); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -1, 0); })); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -4, 1); })); + Require(sceKernelLseek(directory, 0, 1) == 3); + Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, 1, 1); })); + Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto afterQuery = ReadAll(directory); + Require(afterQuery == rest); + + std::array tiny{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + { std::ofstream file(root / "d"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 1) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c", "d"})); + Require(sceKernelClose(directory) == 0); + + const int reused = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(reused == directory); + Require(sceKernelLseek(reused, 5, 0) == 5); + Require(sceKernelLseek(reused, 0, 1) == 5); + Require(sceKernelLseek(reused, 0, 2) == 0); + Require(sceKernelClose(reused) == 0); std::filesystem::remove_all(root); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 81478df5a..dd42eaca2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries`; `lseek` on them takes `SEEK_SET` and `SEEK_CUR` offsets as entry indexes and `SEEK_END` throws. Offset 0 rewinds and takes a new snapshot, as FreeBSD `getdirentries(2)` allows. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries` and on `lseek` with `SEEK_SET` or `SEEK_CUR` to a non-zero index; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From 6db8c81aae164b3fac732593fd1c0f5e8dda1c22 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:48:59 +0300 Subject: [PATCH 038/277] docs(technical-debt): describe the Windows directory snapshot lifetime exactly --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dd42eaca2..dde068966 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries` and on `lseek` with `SEEK_SET` or `SEEK_CUR` to a non-zero index; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From ce0239c940a3811408f9dfff6890c29e7660d209 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 11 Oct 2026 13:59:49 +0200 Subject: [PATCH 039/277] fix(libkernel): return lseek errors for directory descriptors instead of throwing Co-Authored-By: Claude Opus 5.5 --- .../libkernel/File/src/DirectoryDescriptor.cpp | 3 +-- core/libs/prx/libkernel/File/src/Open.cpp | 4 +--- core/libs/tests/GuestDirectoryRewind.cpp | 16 +++------------- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 3b9be91af..9bbe6569b 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -27,7 +27,6 @@ namespace { constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; -constexpr int GuestEoverflow = 84; struct DirectoryState { std::mutex mutex; @@ -99,7 +98,7 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, const auto cursor = static_cast(state.cursor); if (whence == SeekCurrent && offset == 0) return cursor; if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { - errno = GuestEoverflow; + errno = EOVERFLOW; return std::int64_t{-1}; } const std::int64_t target = whence == SeekSet ? offset : cursor + offset; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 50ad1b34a..93151b4d6 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -217,9 +217,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { } #ifdef _WIN32 if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { - if (*directory < 0) { - throw std::runtime_error(std::string(__func__) + ": lseek failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } + if (*directory < 0) return SceIoErrorFromErrno(errno); return *directory; } #endif diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index cfb258e7e..ee576b3c0 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include extern "C" { @@ -28,15 +27,6 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -template -static bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} static std::set ReadAll(int directory) { std::set names; @@ -90,11 +80,11 @@ int main() { Require(ReadAll(directory) == rest); Require(sceKernelLseek(directory, 3, 0) == 3); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -1, 0); })); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -4, 1); })); + Require(sceKernelLseek(directory, -1, 0) == static_cast(0x80020016u)); + Require(sceKernelLseek(directory, -4, 1) == static_cast(0x80020016u)); Require(sceKernelLseek(directory, 0, 1) == 3); Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, 1, 1); })); + Require(sceKernelLseek(directory, 1, 1) == static_cast(0x80020054u)); Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); Require(sceKernelLseek(directory, 0, 0) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dde068966..3f1dfd618 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From 5182c031a7912ef8fea24a9c5a5644b6ed46fd4b Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:45:12 +0200 Subject: [PATCH 040/277] fix(recompiler): merge a loop selection where its arms meet in the iteration A selection in a loop whose arms both reach their nearest common post-dominator within the iteration got that post-dominator as its merge. When an arm can also continue the loop, the post-dominator follows the back edge: the continuing path re-enters the loop header and reaches the outer selection's merge without passing the block where the arms meet. In the loop {1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {} from #2642 (latches 4 and 5) the selections at 1 and 2 both merged at 4. splitSharedMergeBlocks gave 2 a forwarding merge block, recomputed 4 for it because 3 continues through 5, and repeated until "CFG shared merge splitting exceeded budget: originalBlocks=9 currentBlocks=45 splitBudget=36". When that post-dominator is inside the loop construct, findSelectionMerge now takes the arms' nearest common post-dominator over the paths of the iteration that reach it, so paths that leave through the continue block or the loop's merge no longer move the merge past the arms' join. When no arm leaves early the result is the same block as before. The loop structures with two forwarding blocks and no clones or route variables; so do three latches, the selections on the false side, and an if-else whose one arm continues. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 22 +++++++ .../libSceAgcDriver/tests/Structurizer.cpp | 57 +++++++++++++++--- .../ControlFlow/src/Structurizer.cpp | 58 +++++++++++++++++++ 3 files changed, 130 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index e99c97bce..b16d85c39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -520,6 +520,28 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None}, + {"nested selections in a loop whose second latch continues from inside them", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 + s_branch loop +again: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 +loop: + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 latch + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_cmp_gt_u32 vcc, 4, v0 + s_cbranch_vccnz again +latch: + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 loop + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0xbe880380u, 0x7e020280u, 0xbf820002u, 0x4a020282u, 0x80089008u, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860002u, 0x7d880084u, 0xbf87fff8u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fff6u}), Split::None}, {"loop exit to the end of the program beside a kill exit", R"( v_mov_b32 v1, 0 s_mov_b64 s[20:21], exec diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index 9fdc01513..9ed1cd6dc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -104,6 +104,52 @@ void requireExactPostDominators(const ControlFlowGraph& graph, const char* name) } } +void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) { + for (const auto& block : graph.blocks) { + const auto merge = block.terminator.mergeBlock; + if (merge != InvalidControlFlowId && !graph.Dominates(block.id, merge)) { + throw std::runtime_error(std::string(name) + ": header " + std::to_string(block.id) + " does not dominate its merge " + std::to_string(merge)); + } + } +} + +std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + if (block.instructionBegin != block.instructionEnd) return block.instructionBegin / 2; + if (block.terminator.kind != TerminatorKind::Branch || block.terminator.gotoVariable != InvalidControlFlowId) break; + blockId = block.terminator.trueBlock; + } + throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block"); +} + +void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector>& successors, const char* name) { + std::vector seen(successors.size(), false); + for (const auto& block : graph.blocks) { + if (block.instructionBegin == block.instructionEnd) continue; + const auto original = block.instructionBegin / 2; + std::vector targets; + if (block.terminator.kind == TerminatorKind::Branch) targets = {originalBlock(graph, block.terminator.trueBlock)}; + if (block.terminator.kind == TerminatorKind::ConditionalBranch) targets = {originalBlock(graph, block.terminator.trueBlock), originalBlock(graph, block.terminator.falseBlock)}; + if (targets != successors[original]) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets"); + } + seen[original] = true; + } + if (std::find(seen.begin(), seen.end(), false) != seen.end()) { + throw std::runtime_error(std::string(name) + ": an original block is missing"); + } +} + +void requireStructuredLoop(const std::vector>& successors, const char* name) { + auto graph = makeGraph(successors); + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, 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); @@ -238,17 +284,14 @@ int main() { Structurizer{}.Structurize(innerEndingExit); requireExactPostDominators(innerEndingExit, "an inner loop exit that ends the program"); requireStructuredBranches(innerEndingExit, "an inner loop exit that ends the program"); + requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches"); + requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches"); + requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues"); auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}}); Structurizer{}.Structurize(earlyReturn); requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge"); requireStructuredBranches(earlyReturn, "an early return inside a selection that joins its parent's merge"); - for (const auto& block : earlyReturn.blocks) { - const auto merge = block.terminator.mergeBlock; - if (merge != InvalidControlFlowId && !earlyReturn.Dominates(block.id, merge)) { - std::fprintf(stderr, "an early return inside a selection: header %u does not dominate its merge %u\n", block.id, merge); - return 1; - } - } + requireDominatedMerges(earlyReturn, "an early return inside a selection that joins its parent's merge"); std::vector> diamonds; for (std::uint32_t diamond = 0; diamond < 50u; ++diamond) { const auto head = diamond * 3u; diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index e4cb6e53b..4fe6884ca 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -314,6 +314,61 @@ bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& lo return false; } +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 pending = {trueTarget, falseTarget}; + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == globalMerge || contains(reached, blockId) || !isInsideLoopConstruct(graph, loop, blockId)) { + continue; + } + reached.push_back(blockId); + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + std::vector joining = {globalMerge}; + 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)) { + joining.push_back(predecessor); + } + } + } + sortUnique(joining); + if (!contains(joining, trueTarget) || !contains(joining, falseTarget)) { + return globalMerge; + } + std::vector> postDominators(graph.blocks.size()); + for (const auto blockId : joining) { + postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : joining) { + if (blockId == globalMerge) { + continue; + } + auto next = joining; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (contains(joining, successor)) { + next = intersectSorted(next, postDominators[successor]); + } + } + addSortedUnique(next, blockId); + if (next != postDominators[blockId]) { + postDominators[blockId] = std::move(next); + changed = true; + } + } + } + const auto common = intersectSorted(postDominators[trueTarget], postDominators[falseTarget]); + const auto merge = std::find_if(common.begin(), common.end(), [&](std::uint32_t candidate) { + return std::all_of(common.begin(), common.end(), [&](std::uint32_t other) { return contains(postDominators[candidate], other); }); + }); + return merge == common.end() ? globalMerge : *merge; +} + std::vector linearTail(const ControlFlowGraph& graph, std::uint32_t start) { std::vector tail; for (auto blockId = start; !contains(tail, blockId);) { @@ -544,6 +599,9 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return merge; } } + if (trueJoins && isInsideLoopConstruct(graph, *loop, globalMerge)) { + return joinWithinIteration(graph, *loop, trueTarget, falseTarget, globalMerge); + } } return globalMerge; } From 6500daa01264a0168d40851b5fbf7f679e56730d Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:29:40 +0200 Subject: [PATCH 041/277] fix(recompiler): route loop exits that leave the loop before they meet A loop whose exit paths leave the loop construct before they meet got the nearest common post-dominator of its exits as merge, and splitSharedMergeBlocks never separated it. An exit continues the enclosing loop, so its path to that merge runs through the outer header again, or an exit enters a block that a branch before the loop also reaches. splitOneLoopMerge gave the loop a forwarding merge block each round, the other path still bypassed it, and the post-dominator stayed the same block until "CFG shared merge splitting exceeded budget". These shapes from #2642 fail that way on main, on d59a9167, with #2471 and with both: {{1}, {2}, {3, 4}, {2, 5}, {}, {1}} {{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}} {{1, 3}, {2, 4}, {3, 1}, {4}, {}} {{1, 2}, {3, 2}, {4}, {4, 1}, {}} {{1, 2}, {4}, {3, 1}, {4, 2}, {}} When the plain and the privatized pass both fail, Structurize now runs a third pass. It routes such a loop's exits through one junction with routeLoopExits, as loops whose exits have no common post-dominator are routed already, and gives an outer loop whose only latch lies in an inner loop its own continue block. In that pass a loop exit conditional that is neither a direct break or continue nor has an exit tail of its own is structured as a selection, and an exit tail that the other arm reaches is not used as one. Without that, graphs the routing unblocks keep a conditional without merge (the rule from #2762) or a selection entered past its header. Graphs that the first two passes structure are unchanged, and when the third pass fails too, the second pass's error is reported. Differential fuzz against main, reducible graphs with 5 to 9 blocks, each with 0 estimated words per block and again with one block at 1000 words (the privatizeSharedReturns path). Oracle: exact post-dominators, requireStructuredBranches, merges dominated by their headers, the same executions as the original graph, and spirv-val (SPIRV-Tools v2026.4, Vulkan 1.3) on a module emitted from the structured graph. - 200,000 random graphs. main structures 178,743 (0 words) and 178,750 (1000 words); the output is identical on all of them. Of main's 14,256 and 14,311 budget failures 13,527 and 13,596 now structure, and 2,576 and 2,565 of its refusals. No new output is invalid. The invalid outputs are main's 530, plus at 1000 words one graph that d59a9167 already turns from a budget failure into a back edge to a block that is not a loop header. - 24,000 structured programs from orishamir/anyps5-cfgfuzz's generator (if/else, loops, break, continue, return). main structures 21,093 and 21,022; the output is identical. 619 of 2,448 and 634 of 2,429 budget failures now structure. No new output is invalid; the invalid outputs are main's 92. agc_structurizer has the five shapes; requireOriginalBranches now follows route variables. agc_shader_control_flow has two RDNA programs of the first two shapes. The non-GPU tests and the 253 agc_driver GPU tests pass on Windows (RTX 5070 Laptop). Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 41 +++++++++ .../libSceAgcDriver/tests/Structurizer.cpp | 45 ++++++++-- .../include/ControlFlow/Structurizer.hpp | 6 +- .../ControlFlow/src/Structurizer.cpp | 83 +++++++++++++++---- 4 files changed, 147 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index b16d85c39..e471f854e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -542,6 +542,47 @@ latch: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0xbf820002u, 0x4a020282u, 0x80089008u, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860002u, 0x7d880084u, 0xbf87fff8u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fff6u}), Split::None}, + {"an inner loop that ends the program or continues its outer loop", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + v_add_nc_u32 v1, 2, v1 +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz done + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 inner + s_mov_b32 s8, 0 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0x4a020282u, 0x7d880088u, 0xbf860006u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fffau, 0xbe880380u, 0xbf82fff7u}), Split::Route, 0}, + {"an inner loop whose body leaves both loops", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + s_cmp_lt_u32 s8, 64 + s_cbranch_scc0 done +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 done + s_branch inner +latch: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0ac008u, 0xbf84000au, 0x7d880088u, 0xbf860005u, 0x4a020281u, 0x80089008u, 0xbf060208u, 0xbf850004u, + 0xbf82fff9u, 0x4a020282u, 0x80089008u, 0xbf82fff4u}), Split::Route, 0}, {"loop exit to the end of the program beside a kill exit", R"( v_mov_b32 v1, 0 s_mov_b64 s[20:21], exec diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index 9ed1cd6dc..c79f2c4c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -113,24 +114,47 @@ void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) { } } -std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId) { +using Routes = std::map; + +std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes) { for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { const auto& block = graph.FindBlock(blockId); - if (block.instructionBegin != block.instructionEnd) return block.instructionBegin / 2; - if (block.terminator.kind != TerminatorKind::Branch || block.terminator.gotoVariable != InvalidControlFlowId) break; - blockId = block.terminator.trueBlock; + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) 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 && routes.contains(terminator.gotoVariable)) { + blockId = routes.at(terminator.gotoVariable) ? terminator.trueBlock : terminator.falseBlock; + } else { + break; + } } throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block"); } void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector>& successors, const char* name) { std::vector seen(successors.size(), false); - for (const auto& block : graph.blocks) { - if (block.instructionBegin == block.instructionEnd) continue; + Routes entryRoutes; + std::vector> pending{{originalBlock(graph, graph.entryBlock, entryRoutes), entryRoutes}}; + std::vector> visited; + while (!pending.empty()) { + auto state = std::move(pending.back()); + pending.pop_back(); + if (std::find(visited.begin(), visited.end(), state) != visited.end()) continue; + visited.push_back(state); + const auto& block = graph.FindBlock(state.first); const auto original = block.instructionBegin / 2; + std::vector branchTargets; + if (block.terminator.kind == TerminatorKind::Branch) branchTargets = {block.terminator.trueBlock}; + if (block.terminator.kind == TerminatorKind::ConditionalBranch) branchTargets = {block.terminator.trueBlock, block.terminator.falseBlock}; std::vector targets; - if (block.terminator.kind == TerminatorKind::Branch) targets = {originalBlock(graph, block.terminator.trueBlock)}; - if (block.terminator.kind == TerminatorKind::ConditionalBranch) targets = {originalBlock(graph, block.terminator.trueBlock), originalBlock(graph, block.terminator.falseBlock)}; + for (const auto target : branchTargets) { + auto routes = state.second; + const auto reached = originalBlock(graph, target, routes); + targets.push_back(graph.FindBlock(reached).instructionBegin / 2); + pending.emplace_back(reached, std::move(routes)); + } if (targets != successors[original]) { throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets"); } @@ -287,6 +311,11 @@ int main() { requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches"); requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches"); requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues"); + requireStructuredLoop({{1}, {2}, {3, 4}, {2, 5}, {}, {1}}, "an inner loop that returns or continues its outer loop"); + requireStructuredLoop({{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}, "an inner loop that leaves both loops or continues its outer loop"); + requireStructuredLoop({{1, 3}, {2, 4}, {3, 1}, {4}, {}}, "a loop whose latch exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {3, 2}, {4}, {4, 1}, {}}, "a loop whose header exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {4}, {3, 1}, {4, 2}, {}}, "a loop whose header exits into a block the branch before the loop reaches first"); auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}}); Structurizer{}.Structurize(earlyReturn); requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge"); diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index 15318dc57..9e331aa1d 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -10,7 +10,7 @@ public: void Structurize(ControlFlowGraph& graph) const; private: - void structurize(ControlFlowGraph& graph, bool privatizeReturns) const; + void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits) const; void privatizeSharedReturns(ControlFlowGraph& graph) const; void computeDominatorTree(ControlFlowGraph& graph) const; void detectNaturalLoops(ControlFlowGraph& graph) const; @@ -18,8 +18,8 @@ private: void computeBackEdges(ControlFlowGraph& graph) const; void computeStronglyConnectedComponents(ControlFlowGraph& graph) const; void recomputeAnalyses(ControlFlowGraph& graph) const; - void canonicalizeNaturalLoops(ControlFlowGraph& graph) const; - void splitSharedMergeBlocks(ControlFlowGraph& graph) const; + void canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits) const; + void splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits) const; void isolateSemanticLoopHeaders(ControlFlowGraph& graph) const; void orderByDominance(ControlFlowGraph& graph) const; void clearStructuredTerminators(ControlFlowGraph& graph) const; diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 4fe6884ca..dab0136e4 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/ControlFlowHelpers.hpp" #include #include +#include #include #include #include @@ -633,7 +634,7 @@ bool isInnermostLoopControlConditional(const ControlFlowGraph& graph, const Basi return (isControlTarget(trueTarget) && (isControlTarget(falseTarget) || isInsideLoopConstruct(graph, *loop, falseTarget))) || (isControlTarget(falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)); } -std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { if (block.terminator.kind != TerminatorKind::ConditionalBranch) { return InvalidControlFlowId; } @@ -649,7 +650,7 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB } const auto body = trueInBody ? trueTarget : falseTarget; const auto tail = trueInBody ? falseTarget : trueTarget; - if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail)) { + if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail) || (privateTail && reachesWithinIteration(graph, *loop, body, tail))) { return InvalidControlFlowId; } std::vector pending{tail}; @@ -674,6 +675,20 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB return body; } +bool isStructuredLoopExit(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { + if (!isInnermostLoopControlConditional(graph, block)) { + return false; + } + if (!privateTail) { + return true; + } + const auto* loop = findInnermostContainingLoop(graph, block.id); + const auto isControlTarget = [&](std::uint32_t target) { + return target == loop->mergeBlock || target == loop->continueBlock || (block.id == loop->continueBlock && target == loop->headerBlock); + }; + return isControlTarget(block.terminator.trueBlock) || isControlTarget(block.terminator.falseBlock) || exitTailSelectionMerge(graph, block, true) != InvalidControlFlowId; +} + bool mergeLeavesContainingLoop(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t merge) { for (const auto& loop : graph.naturalLoops) { if (loop.headerBlock != header && isInsideLoopConstruct(graph, loop, header) && !isInsideLoopConstruct(graph, loop, merge)) { @@ -1140,6 +1155,26 @@ std::uint32_t nextGotoVariable(const ControlFlowGraph& graph) { return next; } +bool exitsLeaveConstruct(const ControlFlowGraph& graph, const NaturalLoop& loop) { + std::vector visited(graph.blocks.size(), false); + std::vector pending; + pending.insert(pending.end(), loop.exitBlocks.begin(), loop.exitBlocks.end()); + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == loop.mergeBlock || visited[blockId]) { + continue; + } + if (!graph.Dominates(loop.headerBlock, blockId)) { + return true; + } + visited[blockId] = true; + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + return false; +} + std::optional routeLoopExits(const ControlFlowGraph& graph, const NaturalLoop& loop, const std::function& recompute, std::string& refusal) { std::vector exits; for (const auto member : loop.blocks) { @@ -1250,7 +1285,7 @@ bool structurizeEnteredRegion(ControlFlowGraph& graph, std::uint32_t header, std throw std::runtime_error("selection header block " + std::to_string(header) + " has externally entered region block " + std::to_string(entryEdges.front().second) + " (from block " + std::to_string(entryEdges.front().first) + "): " + cloneReason + "; " + junctionReason); } -bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute) { +bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute, bool routeLeavingExits) { std::vector loopHeaders; loopHeaders.reserve(graph.naturalLoops.size()); for (const auto& loop : graph.naturalLoops) { @@ -1271,7 +1306,7 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const for (const auto blockId : selectionHeaders) { const auto& block = graph.FindBlock(blockId); - if (isInnermostLoopControlConditional(graph, block)) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { continue; } @@ -1390,10 +1425,20 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; try { - structurize(graph, false); + structurize(graph, false, false); } catch (const std::runtime_error&) { graph = original; - structurize(graph, true); + try { + structurize(graph, true, false); + } catch (const std::runtime_error&) { + const auto error = std::current_exception(); + graph = original; + try { + structurize(graph, false, true); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } + } } } @@ -1408,14 +1453,14 @@ void Structurizer::privatizeSharedReturns(ControlFlowGraph& graph) const { } } -void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns) const { +void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits) const { recomputeAnalyses(graph); verifyReducibility(graph); if (privatizeReturns) { privatizeSharedReturns(graph); } - canonicalizeNaturalLoops(graph); - splitSharedMergeBlocks(graph); + canonicalizeNaturalLoops(graph, routeLeavingExits); + splitSharedMergeBlocks(graph, routeLeavingExits); isolateSemanticLoopHeaders(graph); orderByDominance(graph); clearStructuredTerminators(graph); @@ -1447,8 +1492,8 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns) c if (block.terminator.kind != TerminatorKind::ConditionalBranch || block.terminator.loopHeader) { continue; } - if (isInnermostLoopControlConditional(graph, block)) { - if (const auto merge = exitTailSelectionMerge(graph, block); merge != InvalidControlFlowId) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { + if (const auto merge = exitTailSelectionMerge(graph, block, routeLeavingExits); merge != InvalidControlFlowId) { exitTails.emplace_back(block.id, merge); } continue; @@ -1634,7 +1679,7 @@ void Structurizer::recomputeAnalyses(ControlFlowGraph& graph) const { computeStronglyConnectedComponents(graph); } -void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { +void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits) const { const auto rewriteBudget = graph.blocks.size() * 2u + 16u; for (std::size_t rewrite = 0; rewrite < rewriteBudget; ++rewrite) { bool changed = false; @@ -1645,7 +1690,10 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { addUnique(latches, edge.sourceBlock); } } - if (latches.size() <= 1u) { + const bool latchInInnerLoop = routeLeavingExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { + return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); + }); + if (latches.size() <= 1u && !latchInInnerLoop) { continue; } @@ -1685,7 +1733,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { const NaturalLoop* unmerged = nullptr; for (const auto& loop : graph.naturalLoops) { - if (loop.exitBlocks.size() > 1u && loop.mergeBlock == InvalidControlFlowId && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { + if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && exitsLeaveConstruct(graph, loop))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { unmerged = &loop; } } @@ -1695,7 +1743,8 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { std::string refusal; auto routed = routeLoopExits(graph, *unmerged, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, refusal); if (!routed) { - throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits with no common post-dominator: " + refusal); + const auto exits = unmerged->mergeBlock == InvalidControlFlowId ? std::string("with no common post-dominator") : "that leave the loop before its merge block " + std::to_string(unmerged->mergeBlock); + throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits " + exits + ": " + refusal); } graph = std::move(*routed); } @@ -1703,13 +1752,13 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { throw std::runtime_error("CFG loop canonicalization exceeded rewrite budget"); } -void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph) const { +void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); 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); }) && !splitOneReturnJoin(graph)) { + if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits) && !splitOneReturnJoin(graph)) { return; } rebuildPredecessors(graph); From 4df387d0c16ffcfefbf8bd246d398dd9f605967e Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:27:56 +0200 Subject: [PATCH 042/277] fix(recompiler): reject a loop-exit pass result that is not structured The third structurize pass, which routes loop exits that leave the loop before they meet, runs only when the plain and the privatized pass throw, and its result was used without a check. Merged with #2471, it turns some of those budget failures into graphs that spirv-val rejects. In this one, selection 12 of the result lies in the selection at 7 and branches to 7's merge block 13: {{1}, {2, 3}, {4}, {5}, {6, 7}, {}, {8, 9}, {10, 11}, {1}, {12, 4}, {5}, {13}, {8, 14}, {15, 16}, {7}, {13}, {4}} block 12 exits the selection headed by block 12, but not via a structured exit hasStructuredConstructs checks the third pass's result against the structured control-flow rules of the SPIR-V specification (2.11), with dominance and post-dominance taken over branch, merge and continue edges. Back edges come only from a loop's continue block. A conditional without a merge breaks or continues its innermost loop. A header strictly dominates its merge, and a loop header its continue block. A construct contains the merge of every header inside it and is entered only through its header. It is left only to its own merge or to the innermost loop's merge or continue block, and a continue construct only to its loop's header or merge. When the result fails, the second pass's error is reported, as when the third pass throws. The first two passes are unchanged. Differential fuzz with the structured-program generator and random reducible graphs. Oracle: exact post-dominators, requireStructuredBranches, merges dominated by their headers, the original's executions, and spirv-val (SPIRV-Tools v2026.4). - Against 9fa7d06a: the same outcome and output on every graph. That covers 100,000 random graphs at 0 words and with one block at 1000, and 30,000 programs at 16 and at 1000 words per block, each also with one block at 1000. - Merged with #2471, the same 30,000 programs: the third pass had made 41, 42 and 45 of them invalid (16 words, 16 with one block at 1000, 1000 words). These end in #2471's own error again, and nothing else changes. The 1, 1 and 18 graphs that d59a9167 merged with #2471 already structures invalidly come from the first two passes and stay as they are. Co-Authored-By: Claude Opus 5.5 --- .../include/ControlFlow/Structurizer.hpp | 1 + .../ControlFlow/src/Structurizer.cpp | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index 9e331aa1d..a2ba90c6e 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -24,6 +24,7 @@ private: void orderByDominance(ControlFlowGraph& graph) const; void clearStructuredTerminators(ControlFlowGraph& graph) const; void verifyReducibility(const ControlFlowGraph& graph) const; + bool hasStructuredConstructs(const ControlFlowGraph& graph) const; }; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index dab0136e4..5d66400ca 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1438,6 +1438,9 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { } catch (const std::runtime_error&) { std::rethrow_exception(error); } + if (!hasStructuredConstructs(graph)) { + std::rethrow_exception(error); + } } } } @@ -1822,4 +1825,89 @@ void Structurizer::verifyReducibility(const ControlFlowGraph& graph) const { } } +bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const { + auto structural = graph; + for (auto& block : structural.blocks) { + if (block.terminator.mergeBlock != InvalidControlFlowId) { + block.successors.push_back(block.terminator.mergeBlock); + } + if (block.terminator.loopHeader) { + block.successors.push_back(block.terminator.continueBlock); + } + } + rebuildPredecessors(structural); + computeDominatorTree(structural); + computePostDominators(structural); + const auto inRegion = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.id, blockId) && !structural.Dominates(header.terminator.mergeBlock, blockId); + }; + const auto inContinue = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.terminator.continueBlock, blockId) && structural.PostDominates(header.terminator.continueBlock, blockId); + }; + const auto inConstruct = [&](const BasicBlock& header, std::uint32_t blockId) { + return inRegion(header, blockId) && !(header.terminator.loopHeader && inContinue(header, blockId)); + }; + const auto innermostLoop = [&](std::uint32_t blockId) { + const BasicBlock* innermost = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && inRegion(header, blockId) && (innermost == nullptr || structural.Dominates(innermost->id, header.id))) { + innermost = &header; + } + } + return innermost; + }; + const auto exitsLoop = [](const BasicBlock* loop, std::uint32_t target) { + return loop != nullptr && (target == loop->terminator.mergeBlock || target == loop->terminator.continueBlock); + }; + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + for (const auto successor : block.successors) { + const auto& target = graph.FindBlock(successor); + if (structural.Dominates(successor, block.id) && (!target.terminator.loopHeader || target.terminator.continueBlock != block.id)) { + return false; + } + } + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && terminator.mergeBlock == InvalidControlFlowId && terminator.trueBlock != terminator.falseBlock) { + const auto* loop = innermostLoop(block.id); + if (!exitsLoop(loop, terminator.trueBlock) && !exitsLoop(loop, terminator.falseBlock)) { + return false; + } + } + if (terminator.mergeBlock == InvalidControlFlowId) { + continue; + } + if (terminator.mergeBlock == block.id || !structural.Dominates(block.id, terminator.mergeBlock)) { + return false; + } + if (terminator.loopHeader && (terminator.continueBlock == terminator.mergeBlock || !structural.Dominates(block.id, terminator.continueBlock))) { + return false; + } + const auto* loop = terminator.loopHeader ? &block : innermostLoop(block.id); + for (const auto& member : graph.blocks) { + if (inConstruct(block, member.id)) { + if (member.id != block.id && member.terminator.mergeBlock != InvalidControlFlowId && !inConstruct(block, member.terminator.mergeBlock)) { + return false; + } + if (member.id != block.id && !std::all_of(member.predecessors.begin(), member.predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor); })) { + return false; + } + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inConstruct(block, successor) || successor == terminator.mergeBlock || exitsLoop(loop, successor); })) { + return false; + } + } else if (terminator.loopHeader && inContinue(block, member.id)) { + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inContinue(block, successor) || successor == block.id || successor == terminator.mergeBlock; })) { + return false; + } + } + } + if (terminator.loopHeader) { + const auto& predecessors = graph.FindBlock(terminator.continueBlock).predecessors; + if (!std::all_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor) || inContinue(block, predecessor); })) { + return false; + } + } + } + return true; +} + } From e6c8e1e9c7b93e8d52798adb6ea5bdeb1156fbe3 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:42:51 -0300 Subject: [PATCH 043/277] fix(recompiler): route shared loop exit selections in the guarded fallback --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 30 +++++++++++++++++++ .../libSceAgcDriver/tests/Structurizer.cpp | 7 +++++ .../ControlFlow/src/Structurizer.cpp | 24 ++++++++++++--- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index e471f854e..9124a643f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -409,6 +409,36 @@ int main(int argc, char** argv) { return failures == 0 ? 0 : 1; } const std::vector programs{ + {"reduced shared exit junction from the public CFG fuzz report", "Public reduced word stream from #2762; synthetic regression, not title data.", + {0x060202f2u, 0x060202f4u, 0x7d880084u, 0xbf870001u, 0xbf820011u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf870005u, + 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870009u, 0xbf82fff3u, 0x060202ffu, 0x3f840000u, 0x7d880087u, 0xbf870001u, + 0xbf820003u, 0x060202ffu, 0x3f860000u, 0xbf82ffebu, 0x060202f6u, 0xe0700000u, 0x80010100u, 0xbf810000u}, Split::None}, + {"both selection arms leave through a shared loop exit tail", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +loop: + s_cmp_lt_u32 s8, 4 + s_cbranch_scc0 done + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 right + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 1, v1 + s_branch latch +right: + v_cmp_gt_u32 vcc, 16, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 2, v1 +latch: + s_add_u32 s8, s8, 1 + s_branch loop +tail: + v_add_nc_u32 v1, 3, v1 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0a8408u, 0xbf84000cu, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860007u, 0x4a020281u, + 0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::Route, 0}, {"shared tail", R"( v_cmp_gt_u32 vcc, 16, v0 s_cbranch_vccz outer_else diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index c79f2c4c9..d2d73a6f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -280,6 +280,13 @@ void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) { int main() { try { + auto largeBlocks = makeGraph({{1}, {2, 3}, {1, 4}, {5, 6}, {1, 7}, {8, 7}, {9, 4}, {10}, {6, 10}, {10}, {}}); + for (auto& block : largeBlocks.blocks) block.estimatedSpirvWords = 1000u; + Structurizer{}.Structurize(largeBlocks); + requireStructuredBranches(largeBlocks, "large loop exit blocks retain the baseline result"); + auto exitJunction = makeGraph({{1}, {2, 3}, {9}, {4, 6}, {5, 9}, {1}, {7, 8}, {9}, {1}, {}}); + Structurizer{}.Structurize(exitJunction); + requireStructuredBranches(exitJunction, "shared loop exit junctions retain structured branches"); auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}); Structurizer{}.Structurize(nested); requireExactPostDominators(nested, "nested selections"); diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 5d66400ca..d74417014 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -535,7 +535,7 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur return *merge; } -std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool routeLeavingExits = false) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); const auto trueTarget = block.terminator.trueBlock; @@ -567,6 +567,9 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return globalMerge; } + if (routeLeavingExits && graph.Dominates(block.id, loop->continueBlock) && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { + return joinWithinIteration(graph, *loop, trueTarget, falseTarget, loop->continueBlock); + } if (isLoopControlGateway(graph, *loop, trueTarget) && graph.Dominates(block.id, trueTarget) && isInsideLoopConstruct(graph, *loop, falseTarget)) { return trueTarget; } @@ -1155,6 +1158,19 @@ std::uint32_t nextGotoVariable(const ControlFlowGraph& graph) { return next; } +bool hasSharedExitSelection(const ControlFlowGraph& graph, const NaturalLoop& loop) { + for (const auto id : loop.blocks) { + const auto& block = graph.FindBlock(id); + if (block.terminator.kind != TerminatorKind::ConditionalBranch) continue; + const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); + if (globalMerge != InvalidControlFlowId && isInsideLoopConstruct(graph, loop, globalMerge)) continue; + for (const auto exit : loop.exitBlocks) { + if (exit != loop.mergeBlock && graph.Dominates(block.id, exit) && reachesWithinIteration(graph, loop, block.terminator.trueBlock, exit) && reachesWithinIteration(graph, loop, block.terminator.falseBlock, exit)) return true; + } + } + return false; +} + bool exitsLeaveConstruct(const ControlFlowGraph& graph, const NaturalLoop& loop) { std::vector visited(graph.blocks.size(), false); std::vector pending; @@ -1310,7 +1326,7 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits); if (merge == InvalidControlFlowId) { continue; } @@ -1502,7 +1518,7 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, b continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits); if (merge == InvalidControlFlowId) { throw std::runtime_error("conditional block " + std::to_string(block.id) + " has no structured merge"); } @@ -1736,7 +1752,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL const NaturalLoop* unmerged = nullptr; for (const auto& loop : graph.naturalLoops) { - if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && exitsLeaveConstruct(graph, loop))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { + if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && (exitsLeaveConstruct(graph, loop) || hasSharedExitSelection(graph, loop)))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { unmerged = &loop; } } From b0c86ec07f7103d0e2829bd30c5c54fa0372eed8 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 15:58:56 -0300 Subject: [PATCH 044/277] fix(recompiler): privatize shared continuations before loop gateways --- core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp | 1 + core/shader/recompiler/ControlFlow/src/Structurizer.cpp | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 9124a643f..8b1f9f2e3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -409,6 +409,8 @@ int main(int argc, char** argv) { return failures == 0 ? 0 : 1; } const std::vector programs{ + {"nested selections share a loop continuation", "Synthetic loop with two nested selections and a shared continue; the inner selection does not dominate the continuation.", + {0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f810000u, 0x7d880084u, 0xbf870001u, 0xbf820018u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf87000cu, 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870003u, 0x060202ffu, 0x3f870000u, 0xbf82fff0u, 0x060202ffu, 0x3f860000u, 0x7d880089u, 0xbf87fff9u, 0xbf820008u, 0x060202ffu, 0x3f830000u, 0x7d880086u, 0xbf870001u, 0xbf820003u, 0x060202ffu, 0x3f840000u, 0xbf82fff0u, 0x060202ffu, 0x3f880000u, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None}, {"reduced shared exit junction from the public CFG fuzz report", "Public reduced word stream from #2762; synthetic regression, not title data.", {0x060202f2u, 0x060202f4u, 0x7d880084u, 0xbf870001u, 0xbf820011u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf870005u, 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870009u, 0xbf82fff3u, 0x060202ffu, 0x3f840000u, 0x7d880087u, 0xbf870001u, diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index d2d73a6f5..385d2830e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -339,6 +339,7 @@ int main() { auto chain = makeGraph(diamonds); Structurizer{}.Structurize(chain); requireExactPostDominators(chain, "a chain of fifty selections"); + requireStructuredLoop({{1}, {2, 8}, {3, 5}, {4, 8}, {7}, {6, 7}, {7, 8}, {1}, {}}, "nested selections sharing a loop continuation"); auto sharedDiscard = makeGraph({{5, 1}, {4, 2}, {7, 3}, {7, 4}, {5}, {7, 6}, {}, {}}); const std::vector sharedDiscardWords{444, 1944, 68, 1128, 8, 2084, 148, 28}; for (auto& block : sharedDiscard.blocks) { diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index d74417014..3cb0b0530 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -567,7 +567,7 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return globalMerge; } - if (routeLeavingExits && graph.Dominates(block.id, loop->continueBlock) && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { + if (routeLeavingExits && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { return joinWithinIteration(graph, *loop, trueTarget, falseTarget, loop->continueBlock); } if (isLoopControlGateway(graph, *loop, trueTarget) && graph.Dominates(block.id, trueTarget) && isInsideLoopConstruct(graph, *loop, falseTarget)) { From 901c6278628fbcb5d5de54b2136a691adda796cc Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 18:37:16 -0300 Subject: [PATCH 045/277] fix(recompiler): preserve the guarded loop exit fallback before shared routing Keep the AlxcBTW #24 fallback as a separate guarded attempt before enabling shared-exit and nested-continuation repairs. Add the reduced padded shader reported by orishamir on #2523 as an end-to-end regression; the abstract graph does not reproduce the same failure. --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 48 +++++++++++++++++++ .../include/ControlFlow/Structurizer.hpp | 6 +-- .../ControlFlow/src/Structurizer.cpp | 38 ++++++++------- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 8b1f9f2e3..8268a3c10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -295,6 +295,53 @@ std::size_t recompile(const std::string& name, std::span co return result.spirv.size(); } +void verifyLargeLoopExitFallback() { + const std::vector code{ + 0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f000000u, 0x060202ffu, 0x3f001000u, 0x060202ffu, 0x3f002000u, + 0x060202ffu, 0x3f003000u, 0x060202ffu, 0x3f004000u, 0x060202ffu, 0x3f005000u, 0x060202ffu, 0x3f006000u, + 0x060202ffu, 0x3f007000u, 0x060202ffu, 0x3f008000u, 0x060202ffu, 0x3f009000u, 0x060202ffu, 0x3f00a000u, + 0x060202ffu, 0x3f00b000u, 0x060202ffu, 0x3f00c000u, 0x060202ffu, 0x3f00d000u, 0x060202ffu, 0x3f00e000u, + 0x060202ffu, 0x3f00f000u, 0x060202ffu, 0x3f010000u, 0x060202ffu, 0x3f810000u, 0x060202ffu, 0x3f000010u, + 0x060202ffu, 0x3f001010u, 0x060202ffu, 0x3f002010u, 0x060202ffu, 0x3f003010u, 0x060202ffu, 0x3f004010u, + 0x060202ffu, 0x3f005010u, 0x060202ffu, 0x3f006010u, 0x060202ffu, 0x3f007010u, 0x060202ffu, 0x3f008010u, + 0x060202ffu, 0x3f009010u, 0x060202ffu, 0x3f00a010u, 0x060202ffu, 0x3f00b010u, 0x060202ffu, 0x3f00c010u, + 0x060202ffu, 0x3f00d010u, 0x060202ffu, 0x3f00e010u, 0x060202ffu, 0x3f00f010u, 0x060202ffu, 0x3f010010u, + 0x7d880084u, 0xbf870025u, 0x060202ffu, 0x3f830000u, 0x060202ffu, 0x3f000030u, 0x060202ffu, 0x3f001030u, + 0x060202ffu, 0x3f002030u, 0x060202ffu, 0x3f003030u, 0x060202ffu, 0x3f004030u, 0x060202ffu, 0x3f005030u, + 0x060202ffu, 0x3f006030u, 0x060202ffu, 0x3f007030u, 0x060202ffu, 0x3f008030u, 0x060202ffu, 0x3f009030u, + 0x060202ffu, 0x3f00a030u, 0x060202ffu, 0x3f00b030u, 0x060202ffu, 0x3f00c030u, 0x060202ffu, 0x3f00d030u, + 0x060202ffu, 0x3f00e030u, 0x060202ffu, 0x3f00f030u, 0x060202ffu, 0x3f010030u, 0xbf820099u, 0x060202ffu, + 0x3f820000u, 0x060202ffu, 0x3f000020u, 0x060202ffu, 0x3f001020u, 0x060202ffu, 0x3f002020u, 0x060202ffu, + 0x3f003020u, 0x060202ffu, 0x3f004020u, 0x060202ffu, 0x3f005020u, 0x060202ffu, 0x3f006020u, 0x060202ffu, + 0x3f007020u, 0x060202ffu, 0x3f008020u, 0x060202ffu, 0x3f009020u, 0x060202ffu, 0x3f00a020u, 0x060202ffu, + 0x3f00b020u, 0x060202ffu, 0x3f00c020u, 0x060202ffu, 0x3f00d020u, 0x060202ffu, 0x3f00e020u, 0x060202ffu, + 0x3f00f020u, 0x060202ffu, 0x3f010020u, 0x7d880085u, 0xbf87004cu, 0x060202ffu, 0x3f850000u, 0x060202ffu, + 0x3f000050u, 0x060202ffu, 0x3f001050u, 0x060202ffu, 0x3f002050u, 0x060202ffu, 0x3f003050u, 0x060202ffu, + 0x3f004050u, 0x060202ffu, 0x3f005050u, 0x060202ffu, 0x3f006050u, 0x060202ffu, 0x3f007050u, 0x060202ffu, + 0x3f008050u, 0x060202ffu, 0x3f009050u, 0x060202ffu, 0x3f00a050u, 0x060202ffu, 0x3f00b050u, 0x060202ffu, + 0x3f00c050u, 0x060202ffu, 0x3f00d050u, 0x060202ffu, 0x3f00e050u, 0x060202ffu, 0x3f00f050u, 0x060202ffu, + 0x3f010050u, 0x7d880088u, 0xbf870001u, 0xbf82ff68u, 0x060202ffu, 0x3f870000u, 0x060202ffu, 0x3f000070u, + 0x060202ffu, 0x3f001070u, 0x060202ffu, 0x3f002070u, 0x060202ffu, 0x3f003070u, 0x060202ffu, 0x3f004070u, + 0x060202ffu, 0x3f005070u, 0x060202ffu, 0x3f006070u, 0x060202ffu, 0x3f007070u, 0x060202ffu, 0x3f008070u, + 0x060202ffu, 0x3f009070u, 0x060202ffu, 0x3f00a070u, 0x060202ffu, 0x3f00b070u, 0x060202ffu, 0x3f00c070u, + 0x060202ffu, 0x3f00d070u, 0x060202ffu, 0x3f00e070u, 0x060202ffu, 0x3f00f070u, 0x060202ffu, 0x3f010070u, + 0xbf820027u, 0x060202ffu, 0x3f840000u, 0x060202ffu, 0x3f000040u, 0x060202ffu, 0x3f001040u, 0x060202ffu, + 0x3f002040u, 0x060202ffu, 0x3f003040u, 0x060202ffu, 0x3f004040u, 0x060202ffu, 0x3f005040u, 0x060202ffu, + 0x3f006040u, 0x060202ffu, 0x3f007040u, 0x060202ffu, 0x3f008040u, 0x060202ffu, 0x3f009040u, 0x060202ffu, + 0x3f00a040u, 0x060202ffu, 0x3f00b040u, 0x060202ffu, 0x3f00c040u, 0x060202ffu, 0x3f00d040u, 0x060202ffu, + 0x3f00e040u, 0x060202ffu, 0x3f00f040u, 0x060202ffu, 0x3f010040u, 0x7d880087u, 0xbf87ff43u, 0xbf82ff8du, + 0x060202ffu, 0x3f860000u, 0x060202ffu, 0x3f000060u, 0x060202ffu, 0x3f001060u, 0x060202ffu, 0x3f002060u, + 0x060202ffu, 0x3f003060u, 0x060202ffu, 0x3f004060u, 0x060202ffu, 0x3f005060u, 0x060202ffu, 0x3f006060u, + 0x060202ffu, 0x3f007060u, 0x060202ffu, 0x3f008060u, 0x060202ffu, 0x3f009060u, 0x060202ffu, 0x3f00a060u, + 0x060202ffu, 0x3f00b060u, 0x060202ffu, 0x3f00c060u, 0x060202ffu, 0x3f00d060u, 0x060202ffu, 0x3f00e060u, + 0x060202ffu, 0x3f00f060u, 0x060202ffu, 0x3f010060u, 0xe0700000u, 0x80010100u, 0xbf810000u, + }; + const std::string name("large loop-exit shader preserves the original guarded fallback"); + static_cast(verifyGraph(name, code)); + 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); @@ -1155,6 +1202,7 @@ done: } try { verifyNullSwappc(); + verifyLargeLoopExitFallback(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index a2ba90c6e..10dac4ee1 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -10,7 +10,7 @@ public: void Structurize(ControlFlowGraph& graph) const; private: - void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits) const; + void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits) const; void privatizeSharedReturns(ControlFlowGraph& graph) const; void computeDominatorTree(ControlFlowGraph& graph) const; void detectNaturalLoops(ControlFlowGraph& graph) const; @@ -18,8 +18,8 @@ private: void computeBackEdges(ControlFlowGraph& graph) const; void computeStronglyConnectedComponents(ControlFlowGraph& graph) const; void recomputeAnalyses(ControlFlowGraph& graph) const; - void canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits) const; - void splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits) const; + void canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const; + void splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const; void isolateSemanticLoopHeaders(ControlFlowGraph& graph) const; void orderByDominance(ControlFlowGraph& graph) const; void clearStructuredTerminators(ControlFlowGraph& graph) const; diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 3cb0b0530..778438930 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -535,7 +535,7 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur return *merge; } -std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool routeLeavingExits = false) { +std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool routeLeavingExits = false, bool routeSharedExits = false) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); const auto trueTarget = block.terminator.trueBlock; @@ -567,7 +567,7 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return globalMerge; } - if (routeLeavingExits && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { + if (routeSharedExits && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { return joinWithinIteration(graph, *loop, trueTarget, falseTarget, loop->continueBlock); } if (isLoopControlGateway(graph, *loop, trueTarget) && graph.Dominates(block.id, trueTarget) && isInsideLoopConstruct(graph, *loop, falseTarget)) { @@ -1301,7 +1301,7 @@ bool structurizeEnteredRegion(ControlFlowGraph& graph, std::uint32_t header, std throw std::runtime_error("selection header block " + std::to_string(header) + " has externally entered region block " + std::to_string(entryEdges.front().second) + " (from block " + std::to_string(entryEdges.front().first) + "): " + cloneReason + "; " + junctionReason); } -bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute, bool routeLeavingExits) { +bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute, bool routeLeavingExits, bool routeSharedExits) { std::vector loopHeaders; loopHeaders.reserve(graph.naturalLoops.size()); for (const auto& loop : graph.naturalLoops) { @@ -1326,7 +1326,7 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const continue; } - const auto merge = findSelectionMerge(graph, block, routeLeavingExits); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { continue; } @@ -1441,16 +1441,22 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; try { - structurize(graph, false, false); + structurize(graph, false, false, false); } catch (const std::runtime_error&) { graph = original; try { - structurize(graph, true, false); + structurize(graph, true, false, false); } catch (const std::runtime_error&) { const auto error = std::current_exception(); graph = original; try { - structurize(graph, false, true); + structurize(graph, false, true, false); + if (hasStructuredConstructs(graph)) return; + } catch (const std::runtime_error&) { + } + graph = original; + try { + structurize(graph, false, true, true); } catch (const std::runtime_error&) { std::rethrow_exception(error); } @@ -1472,14 +1478,14 @@ void Structurizer::privatizeSharedReturns(ControlFlowGraph& graph) const { } } -void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits) const { +void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits) const { recomputeAnalyses(graph); verifyReducibility(graph); if (privatizeReturns) { privatizeSharedReturns(graph); } - canonicalizeNaturalLoops(graph, routeLeavingExits); - splitSharedMergeBlocks(graph, routeLeavingExits); + canonicalizeNaturalLoops(graph, routeLeavingExits, routeSharedExits); + splitSharedMergeBlocks(graph, routeLeavingExits, routeSharedExits); isolateSemanticLoopHeaders(graph); orderByDominance(graph); clearStructuredTerminators(graph); @@ -1518,7 +1524,7 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, b continue; } - const auto merge = findSelectionMerge(graph, block, routeLeavingExits); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { throw std::runtime_error("conditional block " + std::to_string(block.id) + " has no structured merge"); } @@ -1698,7 +1704,7 @@ void Structurizer::recomputeAnalyses(ControlFlowGraph& graph) const { computeStronglyConnectedComponents(graph); } -void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits) const { +void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const { const auto rewriteBudget = graph.blocks.size() * 2u + 16u; for (std::size_t rewrite = 0; rewrite < rewriteBudget; ++rewrite) { bool changed = false; @@ -1709,7 +1715,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL addUnique(latches, edge.sourceBlock); } } - const bool latchInInnerLoop = routeLeavingExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { + const bool latchInInnerLoop = routeSharedExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); }); if (latches.size() <= 1u && !latchInInnerLoop) { @@ -1752,7 +1758,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL const NaturalLoop* unmerged = nullptr; for (const auto& loop : graph.naturalLoops) { - if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && (exitsLeaveConstruct(graph, loop) || hasSharedExitSelection(graph, loop)))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { + if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && (exitsLeaveConstruct(graph, loop) || (routeSharedExits && hasSharedExitSelection(graph, loop))))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { unmerged = &loop; } } @@ -1771,13 +1777,13 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL throw std::runtime_error("CFG loop canonicalization exceeded rewrite budget"); } -void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits) const { +void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); 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) && !splitOneReturnJoin(graph)) { + if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits, routeSharedExits) && !splitOneReturnJoin(graph)) { return; } rebuildPredecessors(graph); From 5781203184e28eca57aa6b44fa8e93da8ae0797c Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 22:10:59 -0300 Subject: [PATCH 046/277] test(shader): expect main to merge the shared exit tail without routing --- core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 8268a3c10..dd1769a8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -487,7 +487,7 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0x7e020280u, 0xbe880380u, 0xbf0a8408u, 0xbf84000cu, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860007u, 0x4a020281u, - 0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::Route, 0}, + 0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::None}, {"shared tail", R"( v_cmp_gt_u32 vcc, 16, v0 s_cbranch_vccz outer_else From b23b54a6d71087468b6616cab52a91daf3d9a7cd Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 22:19:56 -0300 Subject: [PATCH 047/277] perf(shader): avoid rerouting synthetic loop-exit forwarders --- core/shader/recompiler/ControlFlow/src/Structurizer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 778438930..787da0dc5 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1165,6 +1165,8 @@ bool hasSharedExitSelection(const ControlFlowGraph& graph, const NaturalLoop& lo const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); if (globalMerge != InvalidControlFlowId && isInsideLoopConstruct(graph, loop, globalMerge)) continue; for (const auto exit : loop.exitBlocks) { + const auto& forwarder = graph.FindBlock(exit); + if (forwarder.instructionBegin == forwarder.instructionEnd && forwarder.terminator.kind == TerminatorKind::Branch && forwarder.terminator.gotoVariable != InvalidControlFlowId && forwarder.terminator.trueBlock == loop.mergeBlock) continue; if (exit != loop.mergeBlock && graph.Dominates(block.id, exit) && reachesWithinIteration(graph, loop, block.terminator.trueBlock, exit) && reachesWithinIteration(graph, loop, block.terminator.falseBlock, exit)) return true; } } From 3a80442635f6dc51dae4acaa6c236f65d8cb26a8 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 22:19:31 -0300 Subject: [PATCH 048/277] fix(shader): validate conditional exits before accepting every fallback --- .../prx/libSceAgcDriver/tests/ControlFlow.cpp | 45 +++++++++++++++++++ .../ControlFlow/src/Structurizer.cpp | 11 +++++ 2 files changed, 56 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index dd1769a8c..030af961d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -342,6 +342,50 @@ void verifyLargeLoopExitFallback() { static_cast(recompile(name, code, 64u)); } +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}; + std::vector code; + std::array starts{}; + std::vector> patches; + for (std::size_t at = 0; at < order.size(); ++at) { + const auto block = order[at]; + starts[block] = code.size(); + code.insert(code.end(), {0x060202ffu, 0x3f800000u + block * 0x10000u}); + for (std::uint32_t padding = 0; padding < 17; ++padding) code.insert(code.end(), {0x060202ffu, 0x3f000000u + padding * 0x1000u + block * 0x10u}); + const auto next = at + 1 < order.size() ? order[at + 1] : none; + const auto [first, second] = targets[block]; + if (first == none) { + code.insert(code.end(), {0xe0700000u, 0x80010100u, 0xbf810000u}); + continue; + } + if (second != none) { + code.push_back(0x7d880080u + block + 3u); + patches.emplace_back(code.size(), first); + code.push_back(0xbf870000u); + } + const auto branch = second != none ? second : first; + if (branch != next) { + patches.emplace_back(code.size(), branch); + code.push_back(0xbf820000u); + } + } + 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"); + code[instruction] |= static_cast(displacement) & 0xffffu; + } + 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 verifyProgram(const Program& program) { const std::string name(program.name); const auto result = verifyGraph(name, program.code); @@ -1203,6 +1247,7 @@ done: try { verifyNullSwappc(); verifyLargeLoopExitFallback(); + verifyNestedExitForwarders(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 787da0dc5..fcdb1698f 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1444,10 +1444,12 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; try { structurize(graph, false, false, false); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); } catch (const std::runtime_error&) { graph = original; try { structurize(graph, true, false, false); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); } catch (const std::runtime_error&) { const auto error = std::current_exception(); graph = original; @@ -1850,6 +1852,15 @@ void Structurizer::verifyReducibility(const ControlFlowGraph& graph) const { } bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const { + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + if (terminator.kind != TerminatorKind::ConditionalBranch || terminator.loopHeader || terminator.mergeBlock != InvalidControlFlowId || terminator.trueBlock == terminator.falseBlock) continue; + const BasicBlock* loop = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && graph.Dominates(header.id, block.id) && !graph.Dominates(header.terminator.mergeBlock, block.id) && (loop == nullptr || graph.Dominates(loop->id, header.id))) loop = &header; + } + if (loop == nullptr || (terminator.trueBlock != loop->terminator.mergeBlock && terminator.trueBlock != loop->terminator.continueBlock && terminator.falseBlock != loop->terminator.mergeBlock && terminator.falseBlock != loop->terminator.continueBlock)) return false; + } auto structural = graph; for (auto& block : structural.blocks) { if (block.terminator.mergeBlock != InvalidControlFlowId) { From 3943287d63f7fea8e45de0d76a62334f9f09069a Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sat, 10 Oct 2026 23:10:28 -0300 Subject: [PATCH 049/277] refactor(shader): drop the overlapping global loop join fallback --- core/shader/recompiler/ControlFlow/src/Structurizer.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index fcdb1698f..f3a5b7f6f 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -603,9 +603,6 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return merge; } } - if (trueJoins && isInsideLoopConstruct(graph, *loop, globalMerge)) { - return joinWithinIteration(graph, *loop, trueTarget, falseTarget, globalMerge); - } } return globalMerge; } From db1f3c38ef9e24775eeb4b729448fde35d53288e Mon Sep 17 00:00:00 2001 From: thibaut Date: Sun, 11 Oct 2026 11:40:00 +0200 Subject: [PATCH 050/277] fix(shader): drop masked selects proven unobserved through whole quad mode A lane set contained in X is contained in WQM(X). Containment is proven through BitwiseAnd and through ballots evaluated in the uniform entry blocks, where every invocation is active. Under a whole quad mode mask the masked lanes form whole quads, so implicit-LOD pixel samples, which only read their own quad, do not observe them. This removes the selects that kept the packed pixel ancillary input alive after its fields were extracted in GTA V (PPSA04264) pixel shaders. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 72 ++++++++++++++++++- .../src/MaskedSelectEliminator.cpp | 71 ++++++++++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 17c06cd64..02a244738 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -34,6 +34,12 @@ struct Builder { 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 = 0) { auto& created = program.CreateValue(opcode, type, flags); for (auto* argument : arguments) created.AddArgument(argument); @@ -592,14 +598,24 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) { + const auto wholeQuads = [](Builder& b, IrValue& live) -> IrValue& { + auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})}); + auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})}); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)})); + }; + + const auto sample = [&](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw, bool wholeQuadExec = false) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; memory.kind = ResourceKind::Image; memory.imageSampleFlags = sampleFlags; b.program.Resources().memoryInfo.push_back(memory); - auto& exec = b.mask(16u); + auto& exec = wholeQuadExec ? wholeQuads(b, b.mask(16u)) : b.mask(16u); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); @@ -618,6 +634,9 @@ int main() { return !sample(IrShaderStage::Pixel, 0u); }); + passed &= run("an implicit-LOD pixel sample under whole quad mode reads no masked quad and drops the select", [&] { + return sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageSampleRaw, true); + }); passed &= run("an explicit-LOD sample reads only its lane and drops the select", [&] { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); @@ -697,5 +716,54 @@ int main() { return !guarded(IrOpcode::SharedAtomicIAdd32, true, false); }); + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select in every stage", [&] { + const auto widened = [&](IrShaderStage stage) { + Builder b; + b.program.Resources().stage = stage; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }; + return widened(IrShaderStage::Pixel) && widened(IrShaderStage::Compute); + }); + + passed &= run("a write under whole quad mode read by a lane outside the widened mask keeps the select in compute", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Compute; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }); + + passed &= run("a write under whole quad mode read by other lanes keeps the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + return passed ? 0 : 1; } diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index e500ead2a..601dedf05 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -271,6 +271,64 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } +using BlockSet = std::unordered_set; + +BlockSet uniformEntryBlocks(const IrProgram& program) { + BlockSet result; + if (program.BlockOrder().empty()) return result; + const IrBlock* block = program.BlockOrder().front(); + while (block->Predecessors().empty() || (block->Predecessors().size() == 1u && result.contains(block->Predecessors().front()))) { + if (!result.insert(block).second || block->Successors().size() != 1u) break; + block = block->Successors().front(); + } + return result; +} + +bool sameCondition(const IrValue* condition, const IrValue* predicate, std::uint32_t depth) { + condition = condition->Resolve(); + predicate = predicate->Resolve(); + if (condition == predicate || (predicate->HasImmediate() && predicate->Type() == IrType::Bool && predicate->ImmediateBool())) return true; + if (depth > DepthLimit) return false; + return condition->Opcode() == IrOpcode::LogicalAnd && (sameCondition(condition->Argument(0), predicate, depth + 1u) || sameCondition(condition->Argument(1), predicate, depth + 1u)); +} + +bool withinBallotWord(const IrValue* condition, const IrValue* word, std::uint32_t half, const BlockSet& uniform, std::uint32_t depth) { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediate(word->Argument(1)->Resolve(), half)) return false; + const IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && uniform.contains(ballot->Parent()) && sameCondition(condition, ballot->Argument(0), depth + 1u); +} + +bool withinWords(const IrValue* condition, const IrValue* low, const IrValue* high, std::uint32_t waveSize, const BlockSet& uniform, std::uint32_t depth) { + if (depth > DepthLimit) return false; + if (low->Opcode() == IrOpcode::CompositeExtractU32x4) { + return withinBallotWord(condition, low, 0u, uniform, depth) && (waveSize == 32u || withinBallotWord(condition, high, 1u, uniform, depth)); + } + if (low->Opcode() != IrOpcode::BitwiseAnd32) return false; + const IrValue* lowLeft = low->Argument(0)->Resolve(); + const IrValue* lowRight = low->Argument(1)->Resolve(); + if (waveSize == 32u) return withinWords(condition, lowLeft, high, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, high, waveSize, uniform, depth + 1u); + if (high->Opcode() != IrOpcode::BitwiseAnd32) return false; + const IrValue* highLeft = high->Argument(0)->Resolve(); + const IrValue* highRight = high->Argument(1)->Resolve(); + return (withinWords(condition, lowLeft, highLeft, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highRight, waveSize, uniform, depth + 1u)) || + (withinWords(condition, lowLeft, highRight, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highLeft, waveSize, uniform, depth + 1u)); +} + +const IrValue* wholeQuadSource(const IrValue* mask, std::uint32_t waveSize) { + const auto bit = threadBit(mask, waveSize); + if (!bit || bit->low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediate(bit->low->Argument(1)->Resolve(), 0u)) return nullptr; + const IrValue* wqm = bit->low->Argument(0)->Resolve(); + if (waveSize != 32u && (bit->high->Opcode() != IrOpcode::CompositeExtractU64 || bit->high->Argument(0)->Resolve() != wqm || !isImmediate(bit->high->Argument(1)->Resolve(), 1u))) return nullptr; + if (wqm->Opcode() != IrOpcode::WqmU64) return nullptr; + const IrValue* source = wqm->Argument(0)->Resolve(); + return source->Opcode() == IrOpcode::CompositeConstructU64 ? source : nullptr; +} + +bool withinQuadsOf(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, const BlockSet& uniform) { + const IrValue* source = wholeQuadSource(mask, waveSize); + return source != nullptr && withinWords(condition, source->Argument(0)->Resolve(), source->Argument(1)->Resolve(), waveSize, uniform, 0u); +} + enum class Region : std::uint8_t { Unreached, Once, Loop }; Region regionOf(const std::vector& regions, const IrBlock* block) { @@ -297,7 +355,7 @@ struct ImplicationKeyHash { class Implications { public: - Implications(const std::vector& regions, std::uint32_t waveSize, std::size_t& work) : regions(regions), waveSize(waveSize), work(work) {} + Implications(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, std::size_t& work) : regions(regions), uniform(uniform), waveSize(waveSize), work(work) {} void Reset(const IrValue* writeMask, bool acrossLoops) { mask = writeMask; @@ -318,7 +376,9 @@ public: --work; const Attempt attempt = assume(key); bool result = false; - if (condition->Opcode() == IrOpcode::LogicalAnd) { + if (withinQuadsOf(condition, mask, waveSize, uniform)) { + result = true; + } else if (condition->Opcode() == IrOpcode::LogicalAnd) { result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); } else if (condition->IsPhi()) { result = merges(*condition); @@ -414,6 +474,7 @@ private: } const std::vector& regions; + const BlockSet& uniform; std::uint32_t waveSize; std::size_t& work; const IrValue* mask = nullptr; @@ -474,12 +535,13 @@ std::size_t walkLimit(const IrProgram& program) { class MaskedLaneWalk { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), limit(walkLimit(program)), implications(regions, program.WaveSize(), work) {} + MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {} bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; work = limit; implications.Reset(mask, acrossLoops); + const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; startVisits(); pending.clear(); pending.push_back(&select); @@ -493,7 +555,7 @@ public: if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; + if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; if (firstVisit(*user)) { if (work == 0) return false; --work; @@ -521,6 +583,7 @@ private: const IrProgram& program; const std::vector& regions; + BlockSet uniform; std::size_t limit; std::size_t work = 0; Implications implications; From a0e3d2098de3792361d6fd91c8d377c19690b120 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 13:09:21 +0000 Subject: [PATCH 051/277] fix(shader): omit sRGB variants for comparisons --- .../shader/recompiler/Optimization/src/ResourceMaterializer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 2822f4124..b143b78d1 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -735,7 +735,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } - if (image.srgbDecodeFormats != 0u && (image.srgbDecodeCompatible || image.depthCompare) && !storage && !image.packed) { + if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !image.depthCompare && !storage && !image.packed) { const auto count = modes.size(); for (std::size_t index = 0; index < count; ++index) { const auto& base = modes[index]; From 9f3cc31f77d044c8d3a694e59250e3bef9f2b188 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:09:35 +0200 Subject: [PATCH 052/277] feat(libSceRazorCpu): add sceRazorCpuPushMarker PPSA04540 (Rise of the Ronin) imports it. Like sceRazorCpuPushMarkerStatic it does nothing without a capture and returns 0. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRazorCpu/Export.cpp | 7 +++++++ core/libs/tests/GuestRazorCpu.cpp | 3 +++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index c837587a5..7b418022c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -24,6 +24,13 @@ int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { return 0; } +int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t flags) { + (void)label; + (void)color; + (void)flags; + return 0; +} + int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, uint32_t color, uint32_t flags) { (void)name; (void)color; diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index f8fdab4d8..e4af18eed 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -7,6 +7,7 @@ std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); +int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); @@ -29,6 +30,8 @@ int main() { Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPopMarker() == 0); + Require(sceRazorCpuPushMarker("copied", 0xff00ff00u, 1) == 0); + Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..b84b39c4c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -248,6 +248,7 @@ Throughout the project, every function at every stage either **does exactly what - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info - [sceRazorCpuFlushOccurred](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture +- [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [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 From 295e48f0593d4c957113813556689234f3e4f1e3 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:11:24 +0200 Subject: [PATCH 053/277] feat(libSceNgs2): export sceNgs2ReportRegisterHandler and sceNgs2ReportUnregisterHandler PPSA04540 (Rise of the Ronin) imports both and now loads past them; calling either throws until NGS2 produces reports. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/src/Unimplemented.cpp | 15 +++++++++ .../libSceNgs2.native/tests/Ngs2Report.cpp | 33 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 8d100ce35..79d43e8ae 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 62b848071..3287f78d9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -37,6 +37,21 @@ int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) return 0; } +int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { + (void)report_type; + (void)handler; + (void)user_data; + (void)out_handle; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { + (void)report_handle; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp new file mode 100644 index 000000000..f76d61f46 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp @@ -0,0 +1,33 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t reportType, void* handler, std::uintptr_t userData, std::uintptr_t* outHandle); +int APS5_VABI sceNgs2ReportUnregisterHandler(std::uintptr_t reportHandle); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + std::uintptr_t handle = 0x1234; + Require(Throws([&] { sceNgs2ReportRegisterHandler(5, nullptr, 0, &handle); })); + Require(handle == 0x1234); + Require(Throws([] { sceNgs2ReportUnregisterHandler(0x1234); })); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..7e5bdba43 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -278,6 +278,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) - [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 From 83c9eb7af102d6cb9265fe09ea70faf1acf63cb3 Mon Sep 17 00:00:00 2001 From: Superhe11 Date: Sun, 11 Oct 2026 13:39:37 +0300 Subject: [PATCH 054/277] fix(agc): keep uncached storage images out of the resource cache --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 42 ++++--- .../tests/execution/SampledCacheBudget.cpp | 106 +++++++++++++++++- 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..7d8fd21b5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1046,8 +1046,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) + add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) + add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index dbfdf1581..daa271581 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -159,6 +159,7 @@ public: std::vector> StorageImages() const; std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } bool ReadsImage(const StorageTexture* image) const; + bool HasCachedStorageImages() const; const std::vector& LayoutKey() const { return layoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..bfee2423e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -969,18 +969,29 @@ void ClearCachedTextures(VkDevice device) { } } auto& storage = StorageTextures(); - std::lock_guard lock(storage.mutex); - for (auto it = storage.entries.begin(); it != storage.entries.end();) { - if (it->key.device != device) { - ++it; - continue; + std::vector> cleared; + { + std::lock_guard lock(storage.mutex); + for (auto it = storage.entries.begin(); it != storage.entries.end();) { + if (it->key.device != device) { + ++it; + continue; + } + cleared.push_back(it->texture); + it->texture->SetCached(false); + noteUncached(it->texture); + storage.bytes -= it->accounted; + storage.index.erase(it->key); + storage.byImage.erase(it->texture.get()); + it = storage.entries.erase(it); + } + } + for (const auto& image : cleared) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); } - it->texture->SetCached(false); - noteUncached(it->texture); - storage.bytes -= it->accounted; - storage.index.erase(it->key); - storage.byImage.erase(it->texture.get()); - it = storage.entries.erase(it); } } @@ -2524,6 +2535,7 @@ void ResourceCache::noteMiss(const Key& key) { void ResourceCache::Insert(const Key& key, std::shared_ptr resources, std::vector>* evicted) { std::lock_guard lock(mutex); + if (!resources->HasCachedStorageImages()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -2531,9 +2543,6 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr reso } entries.emplace_front(key, std::move(resources)); index.emplace(key, entries.begin()); - // Entries pin their textures and storage images past the texture caches' budgets, so the bound - // stays modest: APS5_RESOURCE_CACHE_ENTRIES (default 1024) covers several frames of distinct - // dispatch and draw content. static const std::size_t capacity = [] { const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES"); const auto parsed = value ? std::strtoull(value, nullptr, 10) : 1024ull; @@ -3526,6 +3535,11 @@ std::vector> ShaderResources::StorageImages() const { return images; } +bool ShaderResources::HasCachedStorageImages() const { + if (std::any_of(storageTextures.begin(), storageTextures.end(), [](const auto& image) { return image != nullptr && !image->Cached(); })) return false; + return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index e7318fa76..8fba8966d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -5,6 +5,8 @@ #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/TextureCache.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #include @@ -45,7 +47,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u; class Device { public: - Device() { + explicit Device(bool resourceImages = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -93,6 +95,11 @@ public: queue.pQueuePriorities = &priority; VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; + if (resourceImages) { + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); + enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; + } address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { @@ -207,6 +214,93 @@ GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) return DecodeTextureResource(words); } +std::shared_ptr ImageResources(const Context& context, const Block& block, std::size_t first, std::uint32_t count, bool sampled) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = count; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(count, false); + binding.imageSamplers.assign(count, 0u); + for (std::uint32_t element = 0; element < count; ++element) { + const auto words = Descriptor(block.Surface(first + element)); + binding.guestDescriptor.insert(binding.guestDescriptor.end(), words.begin(), words.end()); + } + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + return std::make_shared(context, shader); +} + +bool resourceCacheTests(Context context, const std::string& mode) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + Block block(context); + if (context.hostImportAlignment == 0 || HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) { + std::puts("skipped, resource cache tests require host imports"); + return false; + } + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + if (mode == "resource-clear") { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + const std::weak_ptr image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; + cache.Insert({1}, storage); + cache.Insert({2}, sampled); + Require(cache.Size() == 2, "the resource cache did not accept the live descriptor sets"); + sampled.reset(); + ClearCachedTextures(context.device); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == nullptr && cache.Size() == 0, "clearing storage images left their resources cached"); + Require(sampledSet.expired(), "cache clear kept the sampled descriptor set alive"); + Require(!image.expired() && !storageSet.expired(), "cache clear destroyed an active caller's resources"); + storage.reset(); + Require(image.expired() && storageSet.expired(), "cleared storage image outlived its last active resource"); + } else if (mode == "resource-oversized") { + for (const bool sampledImages : {false, true}) { + const std::weak_ptr early = CachedStorageSurface(context, Resource(block.Surface(0))); + auto oversized = ImageResources(context, block, 0, static_cast(Surfaces), sampledImages); + const auto image = early.lock(); + Require(image != nullptr && !image->Cached(), "the oversized descriptor set did not evict an early image while building"); + cache.Insert({1}, oversized); + Require(cache.Size() == 0, sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); + oversized.reset(); + ClearCachedTextures(context.device); + } + } else { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + const auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + Require(image != nullptr && image->Cached(), "the resource has no cached storage image"); + for (std::size_t surface = 1; surface < Surfaces && image->Cached(); ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(!image->Cached(), "the test never evicted the oldest storage image"); + auto live = ImageResources(context, block, Surfaces - 1, 1, false); + cache.Insert({1}, live); + Require(cache.Find({1}) == live, "the resource cache rejected a live descriptor set"); + for (const auto& stale : {storage, sampled}) { + cache.Insert({2}, stale); + Require(cache.Find({2}) == nullptr && cache.Size() == 1, "an uncached image was admitted to the resource cache"); + cache.Insert({1}, stale); + Require(cache.Find({1}) == live && cache.Size() == 1, "an uncached image replaced a live resource-cache entry"); + } + } + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + return true; +} + void viewTests(const Context& context) { if (context.hostImportAlignment == 0) { std::puts("host imports unavailable: views of storage images not tested"); @@ -482,6 +576,8 @@ void memoryTests(Context context) { int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; + const std::string mode = argc > 1 ? argv[1] : ""; + const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); #else @@ -489,7 +585,7 @@ int main(int argc, char** argv) { #endif std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(resources); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); @@ -498,6 +594,12 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; + if (resources) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!resourceCacheTests(context, mode)) return VulkanTestSkipped; + std::puts("resource cache tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); From 9c7aa0d61ad44c5f595daa81d0f837b97c2b376a Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:56:51 +0200 Subject: [PATCH 055/277] feat(libc): implement ftrylockfile Porpoise PPSA99764 stops after pthread_attr_* (#3494) with the predicted `FAIL: unresolved ELF import h05OHOMZNMw from libSceLibcInternal.prx` (idx 311, call site 0x8EC37); RetroArch PPSA99169 imports it at idx 448 (call site 0xA4A27). h05OHOMZNMw is ftrylockfile by tools/nid_names.py and aerolib.csv (zecoxao/sce_symbols 2883963a); ps5-payload-dev/sdk f5d40f02 declares `int ftrylockfile(FILE *)` in include/freebsd/stdio.h. ftrylockfile tries the same host CRT stream lock that flockfile, funlockfile and every locking host stdio call on the stream take, so no second lock exists beside the one stdio uses. It returns 0 when the lock is acquired or already owned by the caller (recursive, released by as many funlockfile calls) and -1 when another thread holds it, as FreeBSD 8c639945 lib/libc/stdio/_flock_stub.c:89-104 does. Linux calls glibc ftrylockfile. UCRT has no try variant of _lock_file, so Windows calls TryEnterCriticalSection on the CRITICAL_SECTION that _lock_file enters (__crt_stdio_stream_data::_lock, ucrt/inc/corecrt_internal_stdio.h, same layout in Windows SDK 10.0.19041, 10.0.22621 and 10.0.26100). A null or closed stream throws. The UCRT layout dependency is recorded in TechnicalDebt. guest_standard_streams covers trylock on an unlocked stream, re-entry by the owner, failure from another thread while held (also after one of two unlocks and while another thread holds flockfile), stdio calls from another thread blocking on the trylock-held lock, release order, and null and closed streams. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/RuntimeSupport.cpp | 26 +++++++++++ core/libs/tests/GuestStandardStreams.cpp | 54 +++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 3 files changed, 82 insertions(+) diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 6c54283da..69be0d848 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -12,6 +12,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -32,6 +35,20 @@ std::mutex g_exitMutex; std::vector g_exitDestructors; bool g_exitRunnerRegistered = false; +#ifdef _WIN32 +struct UcrtStream { + char* pointer; + char* base; + int count; + long flags; + long file; + int charBuffer; + int bufferSize; + char* temporaryName; + CRITICAL_SECTION lock; +}; +#endif + } extern "C" { @@ -142,6 +159,15 @@ void APS5_VABI funlockfile_nid_postfix(FileStream* stream) { _Unlockfilelock_nid_postfix(stream); } +int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { + if (stream == nullptr) throw std::invalid_argument("ftrylockfile: null stream"); +#ifdef _WIN32 + return TryEnterCriticalSection(&reinterpret_cast(GetNativeStream(stream))->lock) ? 0 : -1; +#else + return ftrylockfile(GetNativeStream(stream)) == 0 ? 0 : -1; +#endif +} + std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return std::strtoull(str, endptr, base); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index cb428b927..5de08e2cb 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #ifndef _WIN32 #include #endif @@ -55,6 +56,7 @@ int APS5_VABI _Getmbcurmax_nid_postfix(); int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI flockfile_nid_postfix(FileStream*); void APS5_VABI funlockfile_nid_postfix(FileStream*); +int APS5_VABI ftrylockfile_nid_postfix(FileStream*); } static void Require(bool value) { if (!value) std::abort(); } static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) { @@ -134,6 +136,57 @@ static void CheckUnlockedStreams() { Require(std::filesystem::remove(filename)); } +static void CheckTryLock() { + FileStream stream(std::tmpfile()); + const auto tryFromOtherThread = [&] { + int result = 0; + std::thread other([&] { + result = ftrylockfile_nid_postfix(&stream); + if (result == 0) funlockfile_nid_postfix(&stream); + }); + other.join(); + return result; + }; + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(tryFromOtherThread() == -1); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == -1); + std::atomic written = false; + std::thread writer([&] { + Require(fputc_nid_postfix('B', &stream) == 'B'); + written = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!written && fputc_nid_postfix('A', &stream) == 'A'); + funlockfile_nid_postfix(&stream); + writer.join(); + Require(written && tryFromOtherThread() == 0); + std::atomic held = false, release = false; + std::thread owner([&] { + flockfile_nid_postfix(&stream); + held = true; + while (!release) std::this_thread::yield(); + funlockfile_nid_postfix(&stream); + }); + while (!held) std::this_thread::yield(); + const int contended = ftrylockfile_nid_postfix(&stream); + release = true; + owner.join(); + Require(contended == -1 && ftrylockfile_nid_postfix(&stream) == 0); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == 0); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(&stream) == 'A' && fgetc_nid_postfix(&stream) == 'B'); + stream.Close(); + bool threw = false; + try { ftrylockfile_nid_postfix(&stream); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + threw = false; + try { ftrylockfile_nid_postfix(nullptr); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); +} + static void CheckUnlockedStreamLock(bool write) { FileStream stream(std::tmpfile()); Require(std::fputc('A', stream.GetHandle()) == 'A'); @@ -461,5 +514,6 @@ int main() { CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); + CheckTryLock(); return CheckBinaryModes() ? 0 : 1; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..6044e3496 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -347,6 +347,8 @@ Throughout the project, every function at every stage either **does exactly what - [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 + - [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. - [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 from the same thread-local host `std::random_device` as `arc4random_buf`, 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 From b63996f1ff9b6a17cfee3c48fd81bdc6d38ec253 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 13:13:01 +0000 Subject: [PATCH 056/277] fix(agc): read a merged program's vertex tables past its hidden user words A merged (NGG geometry path) program's user data starts with eight hidden words, so the vertex buffer and attribute table offsets of its AGC header, which count from the first user SGPR, were read eight words too early and found zeros. Killzone: Liberation (PPSA11142) stopped on `AGC graphics: null vertex buffer table address` at the first frame of its PSP game. The decoder now adds the hidden word count to the table reads; the registers reported to the recompiler are unchanged, as it already maps them from the first user SGPR. `agc_driver_graphics` gains a merged and a plain layout case. Co-Authored-By: Claude Fable 5.1 --- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 4 +- .../Graphics/include/ShaderInputState.hpp | 2 +- .../Graphics/src/ShaderInputState.cpp | 12 +++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 39 +++++++++++++++++++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index fe033b378..bc0349ddb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& program = programs[i]; 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]); + vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); 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 9b46369ce..b0a111de6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -618,7 +618,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap if (firstUser == 0) userCount += 8; } plan->userData.resize(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { @@ -805,7 +805,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment; 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); + if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); 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/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 671937306..d1202c5fa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -18,7 +18,7 @@ struct DecodeRead { std::uint64_t address; std::vector bytes; }; -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false); +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 486df0e35..8c20d1a06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -162,7 +162,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con }; } -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi) { +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); Shader shader; @@ -185,14 +185,16 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); - if (static_cast(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); + const auto bufferWord = static_cast(vertexBufferReg) + hiddenUserWords; + const auto attribWord = static_cast(vertexAttribReg) + hiddenUserWords; + if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); + if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain"); if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics"); std::array semantics{}; _readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data()); - const auto attribTableAddr = static_cast(userData[static_cast(vertexAttribReg)]) | (static_cast(userData[static_cast(vertexAttribReg) + 1u]) << 32u); - const auto bufferTableAddr = static_cast(userData[static_cast(vertexBufferReg)]) | (static_cast(userData[static_cast(vertexBufferReg) + 1u]) << 32u); + const auto attribTableAddr = static_cast(userData[attribWord]) | (static_cast(userData[attribWord + 1u]) << 32u); + const auto bufferTableAddr = static_cast(userData[bufferWord]) | (static_cast(userData[bufferWord + 1u]) << 32u); if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address"); if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address"); info.fetchEmbedded = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..7dc8b270b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" #include "Optimization/ResourceProgram.hpp" @@ -723,6 +724,43 @@ void ColorPipeBankXorTests() { } } +void HiddenUserWordVertexTablesTests() { + constexpr std::size_t userDataOffset = 288; + constexpr std::size_t directOffset = 352; + constexpr std::size_t semanticOffset = 384; + static std::vector header(semanticOffset + sizeof(ShaderSemantic)); + static std::array bufferTable{0x1000u, 0x20000u, 64u, 0x3000u}; + static std::array attribTable{(0x2cu << 5u)}; + std::array direct{}; + direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4; + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6; + std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct)); + ShaderSemantic semantic{}; + semantic.size_in_elements = 1; + std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic)); + ShaderUserData userData{}; + userData.direct_resource_offset = reinterpret_cast(header.data() + directOffset); + userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; + std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData)); + Shader shader{}; + shader.user_data = reinterpret_cast(header.data() + userDataOffset); + shader.input_semantics = reinterpret_cast(header.data() + semanticOffset); + shader.num_input_semantics = 1; + std::memcpy(header.data(), &shader, sizeof(shader)); + const auto headerAddress = reinterpret_cast(header.data()); + const auto low = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; + const auto high = [](const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); }; + const std::array registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())}; + std::vector merged(8, 0u); + merged.insert(merged.end(), registers.begin(), registers.end()); + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8); + Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words"); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table"); + const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0); + Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved"); +} + void HeaderlessVertexStageTests() { const std::array userData{1, 2, 3, 4}; const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData); @@ -4316,6 +4354,7 @@ int main(int argc, char** argv) { PixelInputLayoutTests(); ColorPipeBankXorTests(); HeaderlessVertexStageTests(); + HiddenUserWordVertexTablesTests(); ComputeScratchTests(); shaderUserDataTailPaddingTests(); InitialContextTests(); From 6db0a4df7c4ce8853023d03330070aedadb19223 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 11:03:19 +0400 Subject: [PATCH 057/277] fix(libkernel): sync read-only and directory descriptors on Windows FreeBSD's fsync and fdatasync succeed on a descriptor opened read-only and on a directory. _commit needs write access, so both returned EBADF there. A read-only descriptor is now flushed through a write handle from ReOpenFile and a directory through a handle opened for it, as sceKernelSync already does. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 22 ++++++++++++++++++++-- core/libs/tests/GuestFilesystem.cpp | 10 ++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..859ef2e46 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -161,6 +161,24 @@ static int NativeFutimes(int descriptor, const KernelTimeval* times) { } return result; } +static int NativeSync(int descriptor) { + constexpr DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + HANDLE handle = INVALID_HANDLE_VALUE; + if (const auto directory = File::DirectoryDescriptorPath(descriptor)) { + handle = CreateFileW(directory->c_str(), GENERIC_WRITE, share, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + const int previous = errno; + if (::_commit(descriptor) == 0) return 0; + const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); + if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } + handle = ReOpenFile(original, GENERIC_WRITE, share, 0); + errno = previous; + } + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_ACCESS_DENIED ? 0 : File::WindowsFileTime::Failure(GetLastError()); + const int result = FlushFileBuffers(handle) ? 0 : File::WindowsFileTime::Failure(GetLastError()); + CloseHandle(handle); + return result; +} static int NativeFlock(int descriptor, int operation) { return File::Flock(descriptor, operation); } @@ -710,7 +728,7 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { int APS5_VABI sceKernelFsync(int fd) { #ifdef _WIN32 - return ::_commit(fd); + return NativeSync(fd); #else return ::fsync(fd); #endif @@ -719,7 +737,7 @@ int APS5_VABI sceKernelFsync(int fd) { int APS5_VABI sceKernelFdatasync(int fd) { if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - if (::_commit(fd) != 0) return SceErrorFromErrno(errno); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else if (::fdatasync(fd) != 0) return SceErrorFromErrno(errno); #endif diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..3937365e6 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -429,6 +429,16 @@ int main() { Require(open_nid_postfix(rootName.c_str(), 0x0200, 0644) == -1 && *__error_nid_postfix() == 21); Require(open_nid_postfix(rootName.c_str(), 0x0002, 0) == -1 && *__error_nid_postfix() == 21); Require(open_nid_postfix(rootName.c_str(), 0x0001, 0) == -1 && *__error_nid_postfix() == 21); + const int syncedDirectory = open_nid_postfix(rootName.c_str(), 0, 0); + Require(syncedDirectory >= 0 && sceKernelFsync(syncedDirectory) == 0 && fdatasync_nid_postfix(syncedDirectory) == 0); + Require(sceKernelFdatasync(syncedDirectory) == 0 && close_nid_postfix(syncedDirectory) == 0); + const int syncedReader = open_nid_postfix(presentName.c_str(), 0, 0); + Require(syncedReader >= 0 && sceKernelFsync(syncedReader) == 0 && fdatasync_nid_postfix(syncedReader) == 0); + Require(sceKernelFdatasync(syncedReader) == 0 && close_nid_postfix(syncedReader) == 0); + Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0400) == 0); + const int syncedLocked = open_nid_postfix(presentName.c_str(), 0, 0); + 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); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); From 88cd5bbde5bc77923522845ae3b7fed3e32a9b17 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:04:57 +0200 Subject: [PATCH 058/277] fix(libSceAgc): accept a hand-built GE_USER_VGPR register offset in sceAgcCreatePrimState EVO Player (PPSA99039), Jelly5 (PPSA99505), Emby5 (PPSA99515) and Nuvio PS5 (PPSA99997) stop on main 0f863c10 with what(): sceAgcCreatePrimState: invalid user configuration register offsets All four build their shader headers by hand (EVO-PLAYER-PS5 evo_agc_shader_header.c, ported from ps5-xash3d-halflife) and store the ge_user_vgpr_en special at 0x25c (GE_USER_VGPR1, value 0) instead of 0x262 (GE_USER_VGPR_EN), as analysed in #3457. They run on the console. OpenAGC's reconstruction of the FW 5.50 sceAgcCreatePrimState (src/game_compat.c at 3c7e8072) copies the four special register pairs verbatim without checking their offsets; KytyPS5's offset checks (src/libs/agc.cpp at 16a7fe68) are EXIT_NOT_IMPLEMENTED guards. The special is now copied at the offset the header stores, not moved to 0x262; the driver reads neither register. GE_CNTL, VGT_SHADER_STAGES_EN and a used VGT_GS_OUT_PRIM_TYPE are still required at their own offsets because the driver reads them there, and the error now names the special and the offset found. With the change all four titles get past sceAgcCreatePrimState and stop in the shader recompiler (GetImageResource dword 3 is not a valid runtime value), as #3457 reported for EVO Player. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit aeaa5080d9595305aa73e74d966f989c88b79839) --- .../prx/libSceAgc/Shader/src/PrimState.cpp | 34 ++++++++++--------- .../prx/libSceAgc/tests/ShaderHelpers.cpp | 26 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index e03b491b2..587ae15a3 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -11,6 +11,18 @@ #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +namespace { + +void RequireSpecialOffset(const ShaderRegister& reg, std::uint32_t expected, const char* name) { + if (reg.offset != expected) { + char message[160]; + std::snprintf(message, sizeof(message), "sceAgcCreatePrimState: %s special register at offset 0x%x, expected 0x%x", name, static_cast(reg.offset), static_cast(expected)); + throw std::runtime_error(message); + } +} + +} + extern "C" { int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, const Shader* hs, const Shader* gs, std::uint32_t prim_type) { @@ -25,32 +37,22 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers"); } (void)GraphicsPrimTypeToGsOut(prim_type); - const auto validStages = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN; }; - if (!validStages(gs) || (hs != nullptr && !validStages(hs))) { - throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); - } - const auto normalizeUserVgprEnable = [](ShaderRegister reg) { - if (reg.offset == ShaderRegs::GE_USER_VGPR1 && reg.value == 0) reg.offset = ShaderRegs::GE_USER_VGPR_EN; - return reg; - }; - const auto gsUserVgprEnable = normalizeUserVgprEnable(gs->specials->ge_user_vgpr_en); - const auto hsUserVgprEnable = hs != nullptr ? normalizeUserVgprEnable(hs->specials->ge_user_vgpr_en) : gsUserVgprEnable; - if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN)) { - throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets"); - } + RequireSpecialOffset(gs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + if (hs != nullptr) RequireSpecialOffset(hs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + RequireSpecialOffset(gs->specials->ge_cntl, ShaderRegs::GE_CNTL, "GE_CNTL"); std::array contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type}; if (hs != nullptr) { contextValues[0].value |= hs->specials->vgt_shader_stages_en.value; } if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) { - if (gs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(gs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); } else if (hs != nullptr) { - if (hs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(hs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); contextValues[1] = hs->specials->vgt_gs_out_prim_type; } else { contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; } - std::array primitiveValues{gs->specials->ge_cntl, hsUserVgprEnable, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; + std::array primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index 2dce67750..ce8451e0e 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); @@ -48,6 +49,30 @@ void OptionalGeometryOutputRegister(ShaderSpecialRegs& special, const Shader& ve special.vgt_shader_stages_en.value = 0; special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2}; } + +void HandBuiltUserVgprRegister() { + using namespace ShaderRegs; + ShaderSpecialRegs handBuilt{}; + handBuilt.ge_cntl = {GE_CNTL, 0x100u}; + handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u}; + handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2}; + handBuilt.ge_user_vgpr_en = {0x25cu, 0}; + Shader preRaster{}; + preRaster.type = static_cast(ShaderBinaryType::Gs); + preRaster.specials = &handBuilt; + std::array context{}; + std::array primitive{}; + preparations = 0; + Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast(GsOutputPrimitiveType::Triangles)); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == 0x25cu && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == 0x25cu && preparedPrimitive[1].value == 0); + handBuilt.ge_cntl.offset = 0x25cu; + std::string message; + try { static_cast(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); } + catch (const std::runtime_error& error) { message = error.what(); } + Require(message.find("GE_CNTL") != std::string::npos && message.find("0x25c") != std::string::npos); +} } extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { @@ -181,6 +206,7 @@ int main() { Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, nullptr) == 0); Require(preparations == 0 && mappings == 0 && links == 0); OptionalGeometryOutputRegister(special, vertex, hull); + HandBuiltUserVgprRegister(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 04123a3c1..bad8319a2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -598,3 +598,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [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 From b1fe54e331336872a0df9b375d5d4953d3092d9c Mon Sep 17 00:00:00 2001 From: DariusHQ Date: Sun, 11 Oct 2026 14:32:28 +0300 Subject: [PATCH 059/277] fix(libc): print 0 for %#.0o of zero on Windows C and FreeBSD's vfprintf print a single 0 when # meets octal zero with a precision of 0. The Windows formatter passed the conversion to the host's printf, which printed nothing; it now asks for one digit instead. --- core/libs/prx/libc/include/WindowsFormatting.hpp | 2 ++ core/libs/tests/Formatting.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 95026c4dc..aedfdc4d7 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -232,6 +232,8 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == 'o' && value == 0 && precisionLimit == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; output.Value(spec + "ll" + conversion, value); } else if (std::strchr("aAeEfFgG", conversion)) { if (length == "L") { diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b6134b7b9..b5ac19205 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -155,6 +155,10 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(snprintf_nid_postfix(buffer, 1, "%d", 123) == 3 && buffer[0] == 0); Require(sprintf_nid_postfix(buffer, "%hhd %hhu %hd %hu %%", 255, 257, 65535, 65537) == 11); Require(std::strcmp(buffer, "-1 1 -1 1 %") == 0); + Require(sprintf_nid_postfix(buffer, "[%#.0o][%#.o][%#5.0o][%-#4.0o][%#.*o][%#.0o][%.0o][%#o][%#.3o]", 0u, 0u, 0u, 0u, 0, 0u, 8u, 0u, 0u, 0u) == 37); + Require(std::strcmp(buffer, "[0][0][ 0][0 ][0][010][][0][000]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0lo][%#.0hho][%#.0x]", 0ull, 0u, 0u) == 8 && std::strcmp(buffer, "[0][0][]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0o][%ls]", 0u, u"x") == 6 && std::strcmp(buffer, "[0][x]") == 0); Require(sprintf_nid_postfix(buffer, "%d %d %d %d %d %d", 1, 2, 3, 4, 5, 6) == 11); Require(std::strcmp(buffer, "1 2 3 4 5 6") == 0); int first = 0, second = 0, third = 0, fourth = 0, fifth = 0, sixth = 0; From d9fe7ee614f89a2b097677be3ba28876bfb8014f Mon Sep 17 00:00:00 2001 From: DariusHQ Date: Sun, 11 Oct 2026 14:33:49 +0300 Subject: [PATCH 060/277] fix(libc): accept the ' flag in Windows formatting FreeBSD's vfprintf takes ' as the thousands-grouping flag. The Windows formatter threw on it as an unsupported conversion. The guest's setlocale only accepts the C locale, which has no thousands separator, so the flag changes nothing: it is dropped before the host's printf sees it, and the host's locale cannot add separators. --- core/libs/prx/libc/include/WindowsFormatting.hpp | 3 ++- core/libs/tests/Formatting.cpp | 4 ++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 95026c4dc..2ecaac172 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -174,7 +174,8 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo continue; } std::string spec = "%"; - while (*format && std::strchr("-+ #0", *format)) spec += *format++; + for (; *format && std::strchr("-+ #0'", *format); ++format) + if (*format != '\'') spec += *format; if (*format == '*') { ++format; const int width = args.Next(); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b6134b7b9..1819b8eca 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -126,6 +126,10 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(std::strcmp(buffer, "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.3Lf %d %.1f", 1.125L, 7, 2.5); Require(std::strcmp(buffer, "1.125 7 2.5") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%'d|%'u|%'.2f|%'-8ld|%'+05d|%'g", 1234567, 4000000000u, 1234567.891, 1234567LL, 42, 1e6) == 50); + Require(std::strcmp(buffer, "1234567|4000000000|1234567.89|1234567 |+0042|1e+06") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'lld %'zu", 1234567890123LL, static_cast(9876543210)) == 24 && std::strcmp(buffer, "1234567890123 9876543210") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'d %ls", 1234567, u"x") == 9 && std::strcmp(buffer, "1234567 x") == 0); char expected[1024]; std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..b0a615682 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -549,7 +549,7 @@ Throughout the project, every function at every stage either **does exactly what - [Buffer loads without GLC or DLC](../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [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). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter +- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments and `%m` throw when a format requires the guest formatter - [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, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` From b9fc4483430b768a7672c8218f01b207f2ede062 Mon Sep 17 00:00:00 2001 From: DariusHQ Date: Sun, 11 Oct 2026 15:05:57 +0300 Subject: [PATCH 061/277] fix(libc): format positional arguments on Windows The guest formatter threw on %n$ and *m$, which FreeBSD's vfprintf and glibc accept. It now reads the arguments a numbered format names into a table first, with the types their conversions give them, and follows FreeBSD's rules for unnumbered conversions and unnamed arguments. --- .../prx/libc/include/WindowsFormatting.hpp | 90 ++++++++++++++++++- core/libs/tests/Formatting.cpp | 25 +++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 2ecaac172..b528b0e4a 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include #include #include #include @@ -14,15 +15,43 @@ namespace LibcDetail { +enum class ArgumentClass : unsigned char { Integer, Double, LongDouble }; + class FormatArguments { VaList args; + std::vector> numbered; + size_t position = 1; public: explicit FormatArguments(const void* source) { std::memcpy(&args, source, sizeof(args)); } + void Number(const std::vector& classes) { + numbered.resize(classes.size()); + for (size_t index = 0; index < classes.size(); ++index) { + const auto store = [&](auto value) { std::memcpy(numbered[index].data(), &value, sizeof(value)); }; + if (classes[index] == ArgumentClass::LongDouble) store(Fetch()); + else if (classes[index] == ArgumentClass::Double) store(Fetch()); + else store(Fetch()); + } + } + + void Seek(size_t index) { position = index; } + + template T At(size_t index) { + if (index == 0 || index > numbered.size()) throw std::out_of_range("Format argument position out of range"); + T value; + std::memcpy(&value, numbered[index - 1].data(), sizeof(value)); + return value; + } + template T Next() { + return numbered.empty() ? Fetch() : At(position++); + } + +private: + template T Fetch() { const void* address; if constexpr (std::is_same_v) { if (args.fp_offset < 176) { @@ -158,11 +187,67 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) { return std::string(padding, ' ') + "0x" + digits; } +constexpr size_t maximumArgumentPosition = 99; + +inline size_t ArgumentPosition(const char*& format) { + const char* end = format; + while (*end >= '0' && *end <= '9') ++end; + if (end == format || *end != '$') return 0; + size_t position = 0; + for (const char* digit = format; digit != end && position <= maximumArgumentPosition; ++digit) + position = position * 10 + static_cast(*digit - '0'); + if (position == 0 || position > maximumArgumentPosition) throw std::invalid_argument("Format argument position out of range"); + format = end + 1; + return position; +} + +inline std::vector NumberedArgumentClasses(const char* format) { + if (!std::strchr(format, '$')) return {}; + std::vector classes; + bool numbered = false; + size_t next = 1; + const auto take = [&](size_t position, ArgumentClass type) { + if (classes.size() < position) classes.resize(position); + classes[position - 1] = type; + }; + while ((format = std::strchr(format, '%')) != nullptr) { + ++format; + if (*format == '%') { + ++format; + continue; + } + if (const size_t position = ArgumentPosition(format)) { + next = position; + numbered = true; + } + while (*format && std::strchr("-+ #0'123456789.*", *format)) { + if (*format++ != '*') continue; + const size_t position = ArgumentPosition(format); + if (position) numbered = true; + take(position ? position : next++, ArgumentClass::Integer); + } + bool longDouble = false; + while (*format && std::strchr("hljztL", *format)) longDouble = *format++ == 'L'; + const char conversion = *format; + if (!conversion) break; + ++format; + if (std::strchr("aAeEfFgG", conversion)) take(next++, longDouble ? ArgumentClass::LongDouble : ArgumentClass::Double); + else if (std::strchr("diouxXcCsSpn", conversion)) take(next++, ArgumentClass::Integer); + } + if (!numbered) classes.clear(); + return classes; +} + inline int FormatWindows(char* buffer, size_t size, const char* format, const void* source, std::string* complete = nullptr) { if (!format || !source) throw std::invalid_argument("Null formatting argument"); const char* const formatStart = format; FormatArguments args(source); + args.Number(NumberedArgumentClasses(format)); FormatOutput output(buffer, size, complete); + const auto star = [&] { + const size_t position = ArgumentPosition(format); + return position ? args.At(position) : args.Next(); + }; while (*format) { const char* literal = format; while (*format && *format != '%') ++format; @@ -173,12 +258,13 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Append(format++, 1); continue; } + if (const size_t position = ArgumentPosition(format)) args.Seek(position); std::string spec = "%"; for (; *format && std::strchr("-+ #0'", *format); ++format) if (*format != '\'') spec += *format; if (*format == '*') { ++format; - const int width = args.Next(); + const int width = star(); if (width < 0) spec += '-'; spec += std::to_string(width < 0 ? -static_cast(width) : width); } else { @@ -189,7 +275,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo ++format; if (*format == '*') { ++format; - const int precision = args.Next(); + const int precision = star(); if (precision >= 0) { spec += "." + std::to_string(precision); precisionLimit = static_cast(precision); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 1819b8eca..39eae700a 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -92,9 +92,7 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "%.*S", 3, u"\u00e9\u20ac") == 2 && std::strcmp(output, "\xc3\xa9") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "[%ls]", static_cast(nullptr)) == 8 && std::strcmp(output, "[(null)]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "a%lcb%C", 0, 0) == 4 && std::memcmp(output, "a\0b\0", 5) == 0); -#ifndef _WIN32 Require(FormatList(output, sizeof(output), "%%ls:%2$d:%1$d", 3, 7) == 7 && std::strcmp(output, "%ls:7:3") == 0); -#endif for (std::size_t capacity = 0; capacity <= 6; ++capacity) { std::memset(output, '!', sizeof(output)); int count = -1; @@ -130,6 +128,29 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(std::strcmp(buffer, "1234567|4000000000|1234567.89|1234567 |+0042|1e+06") == 0); Require(FormatList(buffer, sizeof(buffer), "%'lld %'zu", 1234567890123LL, static_cast(9876543210)) == 24 && std::strcmp(buffer, "1234567890123 9876543210") == 0); Require(FormatList(buffer, sizeof(buffer), "%'d %ls", 1234567, u"x") == 9 && std::strcmp(buffer, "1234567 x") == 0); + Require(sprintf_nid_postfix(buffer, "%2$s %1$d %2$s", 7, "x") == 5 && std::strcmp(buffer, "x 7 x") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%3$.1Lf|%2$.2f|%1$hhd|%4$s", 257, 2.5, 1.5L, "end") == 14); + Require(std::strcmp(buffer, "1.5|2.50|1|end") == 0); + Require(sprintf_nid_postfix(buffer, "%9$d%8$d%7$d%6$d%5$d%4$d%3$d%2$d%1$d", 1, 2, 3, 4, 5, 6, 7, 8, 9) == 9 && std::strcmp(buffer, "987654321") == 0); + Require(FormatList(buffer, sizeof(buffer), "%11$d %10$g %9$g %8$g %7$g %6$g %5$g %4$g %3$g %2$g %1$g", 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11) == 23 && + std::strcmp(buffer, "11 10 9 8 7 6 5 4 3 2 1") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%1$*2$.*3$f][%1$-*2$.1f][%4$*2$c]", 3.5, 7, 2, 'z') == 27); + Require(std::strcmp(buffer, "[ 3.50][3.5 ][ z]") == 0); + Require(sprintf_nid_postfix(buffer, "[%2$*1$d][%2$.*1$d]", -4, 5) == 9 && std::strcmp(buffer, "[5 ][5]") == 0); + Require(sprintf_nid_postfix(buffer, "[%1$*2$d]", 5, 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$ls|%1$C|%3$d", 0x41, u"\u00e9", 7) == 6 && std::strcmp(buffer, "\xc3\xa9|A|7") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$d %d %ls|%*5$d|%1$d", 1, 2, 3, u"x", 4) == 12 && std::strcmp(buffer, "2 3 x| 4|1") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%*3$d]%ls", 5, u"", 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + const char* const invalidPositions[] = {"%0$d%ls", "%100$d%ls", "%*0$d%ls"}; + for (const char* format : invalidPositions) { + bool threw = false; + try { + FormatList(buffer, sizeof(buffer), format, 1, u""); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + } char expected[1024]; std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b0a615682..de76c3028 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -549,7 +549,7 @@ Throughout the project, every function at every stage either **does exactly what - [Buffer loads without GLC or DLC](../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [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). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments and `%m` throw when a format requires the guest formatter +- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); `%m` and argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [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, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` From bad19037a6dbcdba49e7c184bab84f25e69d02a8 Mon Sep 17 00:00:00 2001 From: DariusHQ Date: Sun, 11 Oct 2026 15:41:18 +0300 Subject: [PATCH 062/277] feat(libkernel): implement sendfile PS5MC (PPSA99420) stops at start-up on sendfile. It follows FreeBSD 11: headers, the file range and trailers go to a connected stream socket, the file offset does not move, a non-blocking socket fails with EAGAIN after a partial send, and *sbytes reports what was sent. The checks and the point where *sbytes is written follow kern_sendfile.c. --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 80 ++++++++++++++ core/libs/tests/GuestSendfile.cpp | 118 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 209 insertions(+) create mode 100644 core/libs/tests/GuestSendfile.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..13848914f 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -701,6 +701,16 @@ if(BUILD_TESTING) set_tests_properties(guest_execvp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sendfile_tests ${LIBS_INCLUDE_DIR}/tests/GuestSendfile.cpp) + target_include_directories(guest_sendfile_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sendfile_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sendfile_tests) + add_test(NAME guest_sendfile COMMAND guest_sendfile_tests) + set_tests_properties(guest_sendfile PROPERTIES TIMEOUT 60) + if(WIN32) + set_tests_properties(guest_sendfile PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp) target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b5f852307..ab24707dd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -852,6 +852,86 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, message->controlLength = 0; return received; } +struct GuestSendfileHeaders { + GuestIovec* headers; + int headerCount; + GuestIovec* trailers; + int trailerCount; +}; +static_assert(sizeof(GuestSendfileHeaders) == 32 && offsetof(GuestSendfileHeaders, trailers) == 16); +std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset); +static bool SendfileIovecs(const GuestIovec* iov, int count) { + if (!iov) return true; + if (count < 0 || count > 1024) return false; + std::uint64_t total = 0; + for (int i = 0; i < count; ++i) { + if (iov[i].length > INT_MAX - total) return false; + total += iov[i].length; + } + return true; +} +static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) { + if (!data && length) return 14; + const char* bytes = static_cast(data); + while (length) { + const auto result = ::send(socket.value, bytes, static_cast(std::min(length, INT_MAX)), NativeSendFlags(GuestNoSignal)); + if (result < 0) return NativeError(); + bytes += result; + length -= static_cast(result); + sent += result; + } + return 0; +} +static int SendIovecs(const Socket& socket, const GuestIovec* iov, int count, std::int64_t& sent) { + for (int i = 0; iov && i < count; ++i) + if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error; + return 0; +} +int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset, std::uint64_t length, + const GuestSendfileHeaders* headers, std::int64_t* sbytes, int flags) { + (void)flags; + if (offset < 0) return Fail(22); + if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount))) + return Fail(22); + std::vector chunk; + try { + chunk.resize(0x10000); + } catch (const std::bad_alloc&) { + return Fail(12); + } + const auto read = [&](std::int64_t position, std::uint64_t remaining) { + const auto result = sceKernelPread(file, chunk.data(), static_cast(std::min(remaining, chunk.size())), position); + return result < 0 ? -(result & 0xffff) : result; + }; + std::uint64_t remaining = length ? length : UINT64_MAX; + auto received = read(offset, remaining); + if (received == -9) return Fail(9); + std::int64_t sent = 0; + const auto finish = [&](int error) { + if (sbytes) *sbytes = sent; + return error ? Fail(error) : 0; + }; + if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); + const auto socket = Lookup(descriptor); + if (!socket) return finish(9); + if (socket->type != 1) return finish(22); + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + if (::getpeername(socket->value, reinterpret_cast(&peer), &size)) return finish(57); + if (headers) + if (const int error = SendIovecs(*socket, headers->headers, headers->headerCount, sent)) return finish(error); + while (received > 0) { + if (const int error = SendAll(*socket, chunk.data(), static_cast(received), sent)) return finish(error); + offset += received; + remaining -= static_cast(received); + if (remaining == 0) break; + received = read(offset, remaining); + if (received < 0) return finish(static_cast(-received)); + } + if (headers) + if (const int error = SendIovecs(*socket, headers->trailers, headers->trailerCount, sent)) return finish(error); + return finish(0); +} int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) { if (timeout < -1) return Fail(22); if (count && !descriptors) return Fail(14); diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp new file mode 100644 index 000000000..d8aa5f363 --- /dev/null +++ b/core/libs/tests/GuestSendfile.cpp @@ -0,0 +1,118 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +struct Iovec { + const void* base; + std::uint64_t length; +}; +struct Hdtr { + const Iovec* headers; + int headerCount; + const Iovec* trailers; + int trailerCount; +}; +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } +static std::string Receive(int descriptor, std::size_t length) { + std::string data(length, 0); + std::size_t done = 0; + while (done < length) { + const auto result = recv_nid_postfix(descriptor, data.data() + done, length - done, 0); + Require(result > 0); + done += static_cast(result); + } + return data; +} +static std::string Content(std::size_t size) { + std::string content(size, 0); + for (std::size_t i = 0; i < size; ++i) content[i] = static_cast(i * 7 % 251); + return content; +} +static int OpenWritten(const char* path, const std::string& content) { + { + std::ofstream stream(path, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const int file = open_nid_postfix(path, 0, 0); + Require(file >= 0); + return file; +} +int main() { + const std::string content = Content(200000); + const auto size = static_cast(content.size()); + const int file = OpenWritten("sendfile_probe.bin", content); + int pair[2]; + Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); + const Iovec header[] = {{"GET", 3}, {" ", 1}}; + const Iovec trailer[] = {{"END", 3}}; + const Hdtr parts{header, 2, trailer, 1}; + std::int64_t sent = -1; + Require(sendfile_nid_postfix(file, pair[0], 5, 10, &parts, &sent, 0) == 0 && sent == 17); + Require(Receive(pair[1], 17) == "GET " + content.substr(5, 10) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + std::string rest; + std::thread reader([&] { rest = Receive(pair[1], content.size() - 100); }); + Require(sendfile_nid_postfix(file, pair[0], 100, 0, nullptr, &sent, 0) == 0 && sent == size - 100); + reader.join(); + Require(rest == content.substr(100)); + Require(sendfile_nid_postfix(file, pair[0], size - 4, 1000, nullptr, &sent, 0) == 0 && sent == 4); + Require(Receive(pair[1], 4) == content.substr(content.size() - 4)); + Require(sendfile_nid_postfix(file, pair[0], size + 10, 0, &parts, &sent, 0) == 0 && sent == 7); + Require(Receive(pair[1], 7) == "GET END"); + const Hdtr nullHeaders{nullptr, 5, trailer, 1}; + Require(sendfile_nid_postfix(file, pair[0], 0, 3, &nullHeaders, nullptr, 0) == 0); + Require(Receive(pair[1], 6) == content.substr(0, 3) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], -1, 0, nullptr, &sent, 0), 22) && sent == 77); + const std::vector many(1025, Iovec{"x", 1}); + const Hdtr tooMany{many.data(), 1025, nullptr, 0}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &tooMany, &sent, 0), 22) && sent == 77); + const Hdtr negative{nullptr, 0, trailer, -1}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77); + Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0); + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, datagram, 0, 0, nullptr, &sent, 0), 22) && sent == 0); + const int unconnected = socket_nid_postfix(2, 1, 0); + Require(unconnected >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0); + + const std::string large = Content(32 << 20); + const int largeFile = OpenWritten("sendfile_large.bin", large); + int on = 1; + Require(ioctl_nid_postfix(pair[0], 0x8004667e, &on) == 0); + sent = -1; + Require(Fails(sendfile_nid_postfix(largeFile, pair[0], 0, 0, nullptr, &sent, 0), 35)); + Require(sent > 0 && sent < static_cast(large.size())); + Require(Receive(pair[1], static_cast(sent)) == large.substr(0, static_cast(sent))); + + Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + std::filesystem::remove("sendfile_probe.bin"); + std::filesystem::remove("sendfile_large.bin"); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..ffc09cce8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -588,6 +588,7 @@ Throughout the project, every function at every stage either **does exactly what - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, 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) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. 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. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. 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 throws, since guest signals are not delivered to a chosen thread - [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. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. From 1fe527bbec1bcb0c78678f421cf2d32daa26d365 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Fri, 9 Oct 2026 15:34:55 +0200 Subject: [PATCH 063/277] fix(agc): route registered NGG workgroups Defer registered NGG workgroup preparation until routing is known. Share the routing decision with static ABI resolution and draws. Gate the wave64 fixture on supported mesh subgroups; retain device-independent routing coverage. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../Execution/src/Driver/Draw/DrawDecode.cpp | 11 +--- .../src/Driver/Shaders/ShaderRegistry.cpp | 19 +++++- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 25 ++++++++ .../tests/execution/PreparedShaders.cpp | 58 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 7 files changed, 108 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..e1de66ffb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1105,7 +1105,7 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode STREQUAL "indirect") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect) @@ -1113,6 +1113,8 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred) elseif(preparedMode STREQUAL "deferred_undecodable") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable) + elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) elseif(preparedMode STREQUAL "debug_failure") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration) set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30) @@ -1122,6 +1124,8 @@ if(BUILD_TESTING) set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(preparedMode STREQUAL "uncached") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1") + elseif(preparedMode STREQUAL "registered_workgroup_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1") endif() 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 7ccb5d690..f0ee74af4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -92,6 +92,7 @@ std::shared_ptr ProgramSnapshot(const ShaderRegistry& shad void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); +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/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index ba5510217..2af5a114d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -109,16 +109,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission) { auto product = std::make_shared(); - bool vertexWorkgroup = false; - const auto stages = queue.context.find(0x2d5); - const auto low = queue.shader.find(0xc8); - const auto high = queue.shader.find(0xc9); - if (stages != queue.context.end() && (stages->second & 0x02002024u) == 0x2000u && low != queue.shader.end() && high != queue.shader.end()) { - const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); - const auto front = ProgramSnapshot(*submission.shaders, address, {2}); - vertexWorkgroup = front->header.empty() && ProgramUsesWorkgroup(*front, static_cast((address - front->codeAddress) / sizeof(std::uint32_t))); - } - product->state = Graphics::DecodeState(queue, vertexWorkgroup); + product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders)); DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true); product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); 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 9b46369ce..e81280ca7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -147,6 +147,22 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset } +bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5); + const auto routing = queue.context.find(0x2d5); + if (routing == queue.context.end() || (routing->second & 0x02002024u) != 0x2000u) return false; + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc8); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc9); + const auto low = queue.shader.find(0xc8); + const auto high = queue.shader.find(0xc9); + if (low == queue.shader.end() || high == queue.shader.end()) return false; + require((high->second & ~0xffu) == 0, "reserved graphics program address bits are set"); + 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)); +} + struct ShaderPreparationTransaction::State { struct Change { std::shared_ptr destination; @@ -606,6 +622,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } else { const auto routing = RegisterValue(state.context, 0x2d5); wave = (routing & 0x00400000u) != 0 ? 32u : 64u; + if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr; if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) { if (registration) return {}; auto stageState = state; @@ -883,7 +900,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std return; } DrawDecode decoded{}; - decoded.state.stages = Graphics::DecodeShaderStages(state); + decoded.state.stages = Graphics::DecodeShaderStages(state, VertexWorkgroupRequired(state, *registry)); DecodeGraphicsPrograms(decoded, state, *registry, true, false); auto prepared = PrepareGraphicsStages(decoded, localDevice->Target()); for (auto& stage : prepared) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 10fd4fc9c..da1c9ba0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -629,6 +629,30 @@ 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 testRegisteredVertexWorkgroupRouting() { + using namespace AgcDriver::DriverDetail; + ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}}; + snapshot.header.resize(sizeof(Shader)); + const ShaderRegistry registry{{snapshot.codeAddress, {std::make_shared(std::move(snapshot))}}}; + AgcDriver::QueueState queue{}; + queue.shader = {{0xc8, 0x200}, {0xc9, 0}}; + queue.context[0x2d5] = 0x2000; + check(VertexWorkgroupRequired(queue, registry), "a registered NGG vertex program using LDS did not need a workgroup"); + for (const auto routing : {0u, 0x2020u, 0x2004u, 0x02002000u}) { + queue.context[0x2d5] = routing; + check(!VertexWorkgroupRequired(queue, registry), "legacy, geometry, tessellation or passthrough routing changed to an NGG vertex workgroup"); + } + queue.context[0x2d5] = 0x2000; + auto unaligned = registry; + const auto aligned = unaligned.begin()->second.front(); + ShaderSnapshot shifted{0x1ffff, 0x30000, 2, aligned->code, aligned->header}; + unaligned.clear(); + unaligned[shifted.codeAddress].push_back(std::make_shared(std::move(shifted))); + check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, unaligned)); }).find("not dword aligned") != std::string::npos, "an unaligned registered entry was accepted"); + queue.shader[0xc9] = 0x100; + check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted"); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -722,6 +746,7 @@ int main() { #endif testProgramSnapshots(); testProgramWorkgroupUse(); + testRegisteredVertexWorkgroupRouting(); testEvents(); testValidation(); testClearState(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 93dffc420..4aa54af7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -525,6 +525,54 @@ void Registration(bool indirect) { Require(destination == 0, "failed dispatch executed a subsequent memory write"); } +void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool registrationOnly = false) { + if (!registrationOnly && ((target.subgroupSize != 32u && target.subgroupSize != 64u) || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityMeshShadingEXT) == target.supportedCapabilities.end())) { + std::cout << "registered NGG wave64 preparation not run: requires mesh shading and a 32- or 64-lane host subgroup; subgroup=" << target.subgroupSize << '\n'; + return; + } + alignas(256) static constexpr std::array code{ + 0xd8340000u, 0u, 0xd7600000u, 0x00017f00u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u + }; + struct Header { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1ff, 64}, {0x2ab, 4}, {0x2ce, 0}}}; + ShaderSpecialRegs specials{{0x25b, 0x8040}, {0x2d5, 0x2000}, 0, {}, {}, {0x29b, 2}, {0x260, 0}}; + ShaderUserData userData{}; + } header; + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 2; + header.shader.user_data = &header.userData; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = header.context.size(); + header.shader.specials = &header.specials; + header.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8a, 0x622c0187}, {0x8b, 5u << 19u}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + if (registrationOnly) { + header.shader.user_data = nullptr; + AgcDriverRegisterShader_nid_postfix(&header.shader); + header.specials.vgt_shader_stages_en.value = 0; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "missing AGC user-data header"); + header.shader.user_data = &header.userData; + if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr) { + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "v_readlane_b32 of lane 63 is outside"); + } + std::cout << "registered NGG preparation deferral passed; subgroup=" << target.subgroupSize << '\n'; + return; + } + const std::array primitive{{{0x242, 4}}}; + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + std::cout << "registered NGG wave64 preparation passed; subgroup=" << target.subgroupSize << '\n'; +} + void DeferredRegistration() { alignas(256) std::array code{0xbe842104u, 0xbf810000u}; struct Header { @@ -585,7 +633,14 @@ void DeferredUndecodableRegistration() { int main(int argc, char** argv) { try { - Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable")), "invalid test arguments"); + Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup")), "invalid test arguments"); + if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RegisteredVertexWorkgroup(device->Target(), true); + AgcDriverShutdown_nid_postfix(); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--deferred") { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; DeferredRegistration(); @@ -601,6 +656,7 @@ int main(int argc, char** argv) { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Run(*device); + RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..5708ea62f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -324,6 +324,7 @@ Throughout the project, every function at every stage either **does exactly what - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown +- [NGG vertex workgroups](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [HtileFillCovers](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. From 9b439b30e9d8b1250e2a2b5bfb6aa2a3c5defb67 Mon Sep 17 00:00:00 2001 From: ivica00 Date: Sun, 11 Oct 2026 15:25:25 +0200 Subject: [PATCH 064/277] feat(libc): implement __powisf2 PPSA02456's FMOD plugin imports this float integer-power helper, which libc does not export. --- core/libs/prx/libc/src/Math.cpp | 12 ++++++++++++ core/libs/tests/GuestMath.cpp | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..105066f22 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -121,6 +121,18 @@ float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } float APS5_VABI powf_nid_postfix(float base, float exp) { return std::pow(base, exp); } double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } +float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { + const bool reciprocal = exponent < 0; + float result = 1.0f; + unsigned int magnitude = reciprocal ? 0u - static_cast(exponent) : static_cast(exponent); + while (magnitude != 0u) { + if ((magnitude & 1u) != 0u) result *= base; + magnitude >>= 1u; + if (magnitude == 0u) break; + base *= base; + } + return reciprocal ? 1.0f / result : result; +} float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..682b501f6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -53,6 +53,7 @@ float APS5_VABI log1pf_nid_postfix(float); double APS5_VABI expm1_nid_postfix(double); float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); +float APS5_VABI __powisf2_nid_postfix(float, int); double APS5_VABI ldexp_nid_postfix(double, int); double APS5_VABI scalbn_nid_postfix(double, int); float APS5_VABI scalbnf_nid_postfix(float, int); @@ -491,6 +492,21 @@ int main() { Require(abs_nid_postfix(-42) == 42); Require(abs_nid_postfix(std::numeric_limits::max()) == std::numeric_limits::max()); Require(abs_nid_postfix(std::numeric_limits::min()) == std::numeric_limits::min()); + Require(__powisf2_nid_postfix(2.0f, 10) == 1024.0f); + Require(__powisf2_nid_postfix(2.0f, -10) == 0.0009765625f); + Require(__powisf2_nid_postfix(-2.0f, 3) == -8.0f); + Require(__powisf2_nid_postfix(-2.0f, -3) == -0.125f); + Require(__powisf2_nid_postfix(-2.0f, 4) == 16.0f); + Require(__powisf2_nid_postfix(0.0f, 0) == 1.0f); + Require(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 0) == 1.0f); + Require(std::isnan(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 1))); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::min()) == 1.0f); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::max()) == -1.0f); + Require(__powisf2_nid_postfix(2.0f, std::numeric_limits::min()) == 0.0f); + Require(std::isinf(__powisf2_nid_postfix(0.0f, -1))); + Require(std::signbit(__powisf2_nid_postfix(-0.0f, 3))); + Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); + Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); CheckHyperbolic(); CheckHypotSpecialValue(); } From ae07d4a7a1ceb9c24f70777f09ff2dd2dee2a878 Mon Sep 17 00:00:00 2001 From: ducloc080202 Date: Sun, 11 Oct 2026 20:25:42 +0700 Subject: [PATCH 065/277] fix(libc): quote the executable path in the throw-site test --- core/libs/tests/WindowsExceptions.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 63f8cb1d3..17fefe88b 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -84,13 +84,14 @@ static void testUnhandledThrowSite() { RequireHandle(SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0) != 0); std::array image{}; RequireHandle(GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())) != 0); - const wchar_t* argument = L" throw-uncaught"; + const wchar_t* argument = L"\" throw-uncaught"; std::array command{}; const std::size_t imageLength = std::wcslen(image.data()); const std::size_t argumentLength = std::wcslen(argument); - RequireHandle(imageLength + argumentLength + 1 < command.size()); - std::memcpy(command.data(), image.data(), imageLength * sizeof(wchar_t)); - std::memcpy(command.data() + imageLength, argument, (argumentLength + 1) * sizeof(wchar_t)); + RequireHandle(1 + imageLength + argumentLength + 1 < command.size()); + command[0] = L'"'; + std::memcpy(command.data() + 1, image.data(), imageLength * sizeof(wchar_t)); + std::memcpy(command.data() + 1 + imageLength, argument, (argumentLength + 1) * sizeof(wchar_t)); STARTUPINFOW startup{}; startup.cb = sizeof(startup); startup.dwFlags = STARTF_USESTDHANDLES; From 3faaa4b8f012ed746f8932e1b6b25eb9489a6aa1 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:28:30 +0200 Subject: [PATCH 066/277] feat(libSceJson2): add the sce::Json::Array copy constructor, clear, front and iterator::operator-> PPSA04540 (Rise of the Ronin) imports all four. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceJson2/Export.cpp | 10 ++++++++++ core/libs/tests/GuestJson.cpp | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 672d857ee..808a2ad76 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -529,6 +529,7 @@ std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; @@ -539,6 +540,14 @@ ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, cons ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->it = self->items->end(); return result; } bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array* self) { return self->items->size(); } +void APS5_VABI _ZN3sce4Json5Array5clearEv(Array* self) { + for (auto& item : *self->items) Destroy(item); + self->items->clear(); +} +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array* self) { + if (self->items->empty()) throw std::out_of_range("sce::Json::Array::front: empty array"); + return &self->items->front(); +} const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { if (self->items->empty()) throw std::out_of_range("sce::Json::Array::back: empty array"); return &self->items->back(); @@ -546,6 +555,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { (void)self; } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { ++self->it; return self; } Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { return &*self->it; } +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return &*self->it; } bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { return self->it != other->it; } void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object* self) { Construct(*self); } diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc249..9f4779251 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -35,6 +35,10 @@ void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String*); void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); +void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const Iterator*); void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); @@ -268,6 +272,24 @@ static void ObjectsAndArrays() { _ZNK3sce4Json5Array3endEv(&end, &array); for (; _ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end); _ZN3sce4Json5Array8iteratorppEv(&it)) ++visited; Require(visited == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(&array))); + _ZNK3sce4Json5Array5beginEv(&it, &array); + Require(_ZNK3sce4Json5Array8iteratorptEv(&it) == _ZNK3sce4Json5Array8iteratordeEv(&it)); + Array copy{}; + _ZN3sce4Json5ArrayC1ERKS1_(©, &array); + _ZN3sce4Json5Array5clearEv(&array); + Require(_ZNK3sce4Json5Array4sizeEv(&array) == 0); + bool threw = false; + try { + _ZNK3sce4Json5Array5frontEv(&array); + } catch (const std::out_of_range&) { + threw = true; + } + Require(threw); + Require(_ZNK3sce4Json5Array4sizeEv(©) == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(©))); + Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array4backEv(©)) == 2.5); + _ZN3sce4Json5ArrayD1Ev(©); _ZN3sce4Json5ArrayD1Ev(&array); } From 5334bd178c917482c4718544bdb5511b353f44e3 Mon Sep 17 00:00:00 2001 From: Adelmo Junior Date: Sun, 11 Oct 2026 10:28:39 -0300 Subject: [PATCH 067/277] fix(libSceHttp): retain authority delimiter for partial URI builds --- core/libs/prx/libSceHttp/src/Uri.cpp | 2 +- core/libs/tests/GuestHttp.cpp | 17 ++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 6aac69db3..710a66572 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -182,7 +182,7 @@ int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const std::string uri; if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); - if (!src_element->opaque && option & URI_BUILD_WITH_HOSTNAME) uri.append("//"); + if (!src_element->opaque) uri.append("//"); uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 9d18283f0..0f5e32f4b 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -78,8 +78,19 @@ int main() { Require(sceHttpUriBuild(built, nullptr, required - 1, &element, buildAll) == outOfMemory); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "https://user:secret@example.com:8443/a/c?x=1&y=2#top")); + Require(sceHttpUriBuild(nullptr, &required, 0, &element, 0x01) == 0); + Require(required == sizeof("https://")); + std::memset(built, 'Z', sizeof(built)); + Require(sceHttpUriBuild(built, nullptr, required - 1, &element, 0x01) == outOfMemory); + for (char c : built) Require(c == 'Z'); + Require(sceHttpUriBuild(built, nullptr, required, &element, 0x01) == 0); + Require(Equal(built, "https://") && built[required] == 'Z'); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "//")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x02) == 0); + Require(Equal(built, "//example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x08 | 0x40) == 0); - Require(Equal(built, "/a/c?x=1&y=2")); + Require(Equal(built, "///a/c?x=1&y=2")); Require(sceHttpUriParse(&element, "HTTP://Example.com/", pool, nullptr, sizeof(pool)) == 0); Require(element.port == 80 && Equal(element.username, "") && Equal(element.query, "")); @@ -96,6 +107,10 @@ int main() { Require(Equal(element.username, "someone") && Equal(element.hostname, "example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "mailto:someone@example.com")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x01) == 0); + Require(Equal(built, "mailto:")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "")); Require(sceHttpUriParse(&element, "/path/only?q", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.scheme, "") && Equal(element.hostname, "") && Equal(element.path, "/path/only")); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..66b9a4e3d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -47,6 +47,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info +- [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [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 - [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. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. From 95a92f35e1be8e2cd45e72fb3179049930c10c5e Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:28:58 +0200 Subject: [PATCH 068/277] feat(libSceJson2): add sce::Json::Parser::parse with a read callback PPSA04540 (Rise of the Ronin) imports it. The characters the callback returns are parsed as by the (text, size) overload. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceJson2/Export.cpp | 12 ++++++++++ core/libs/tests/GuestJson.cpp | 33 ++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 46 insertions(+) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 672d857ee..a0c11b225 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -755,6 +755,18 @@ int APS5_VABI _ZNK3sce4Json5Value8toStringERNS0_6StringE(const Value* self, Stri return 0; } +using ParserReadCallback = int (APS5_VABI*)(char&, void*); + +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size); + +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, ParserReadCallback read, void* context) { + if (read == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null read callback"); + std::string text; + char ch = 0; + while (read(ch, context) == 0 && ch != '\0') text.push_back(ch); + return _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(out, text.data(), text.size()); +} + int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); Node parsed{}; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc249..cb6666f12 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -75,6 +75,7 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value*, const Str const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value*, std::size_t); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value*, int (APS5_VABI*)(char&, void*), void*); bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object*); std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); @@ -398,6 +399,37 @@ static void ValueClear() { _ZN3sce4Json5ValueD1Ev(&value); } +struct ReadState { + const char* text; + std::size_t position; + int calls; +}; + +static int APS5_VABI ReadNext(char& ch, void* context) { + auto* state = static_cast(context); + ++state->calls; + if (state->text[state->position] == '\0') { + ch = '\0'; + return -1; + } + ch = state->text[state->position++]; + return 0; +} + +static void StreamParse() { + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + ReadState valid{"{\"list\": [1, 2.5, true]}", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &valid) == 0); + Require(valid.calls == static_cast(std::strlen(valid.text)) + 1); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); + ReadState invalid{"{\"list\": [1, 2.5", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &invalid) < 0); + ReadState trailing{"[1] 2", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) < 0); + _ZN3sce4Json5ValueD1Ev(&root); +} + int main() { ParseAndRoundTrip(); NestingDepth(); @@ -406,4 +438,5 @@ int main() { NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); ValueClear(); + StreamParse(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..fe0f38c8c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -130,6 +130,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached - [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete), [sceContentExportGetProgress, sceContentExportCancel, sceContentExportFromDataWithThumbnail](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sce::Json::Parser::parse with a read callback](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` - [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) From 1fc9b4d93f925568d8fee70d52a06dd57ed85686 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:27:59 +0200 Subject: [PATCH 069/277] feat(libkernel): query real-time alarms with getitimer --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../libs/prx/libkernel/System/src/Signals.cpp | 31 ++++++ core/libs/tests/GuestGetitimer.cpp | 99 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 core/libs/tests/GuestGetitimer.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..bf0ec661b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -394,6 +394,16 @@ if(BUILD_TESTING) set_tests_properties(guest_cond_unowned_mutex PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getitimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetitimer.cpp) + target_include_directories(guest_getitimer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getitimer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getitimer_tests) + add_test(NAME guest_getitimer COMMAND guest_getitimer_tests) + set_tests_properties(guest_getitimer PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getitimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cpu_time_clocks_tests ${LIBS_INCLUDE_DIR}/tests/GuestCpuTimeClocks.cpp) target_include_directories(guest_cpu_time_clocks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cpu_time_clocks_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 62d698299..f4a57b7ff 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -150,6 +150,37 @@ extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) return remaining; } +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output) { + if (which < 0 || which > 2) { + *__error_nid_postfix() = 22; + return -1; + } + if (which != 0) throw std::runtime_error("getitimer: CPU interval timers are not implemented"); + if (output == nullptr) { + *__error_nid_postfix() = 14; + return -1; + } + GuestItimerval result{}; + auto& alarms = Alarms(); + { + std::lock_guard lock(alarms.lock); + const auto now = std::chrono::steady_clock::now(); + if (alarms.deadline && *alarms.deadline > now) { + const auto micros = std::chrono::duration_cast(*alarms.deadline - now).count(); + result.value.tv_sec = micros / 1000000; + result.value.tv_usec = micros % 1000000; + } + } + *output = result; + return 0; +} + struct GuestStack { void* sp; std::size_t size; diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp new file mode 100644 index 000000000..57e484e58 --- /dev/null +++ b/core/libs/tests/GuestGetitimer.cpp @@ -0,0 +1,99 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; + +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" { +int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); +unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +int* APS5_VABI __error_nid_postfix(); +using Handler = void (APS5_VABI *)(int); +Handler APS5_VABI signal_nid_postfix(int, Handler); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::int64_t ReadRemaining() { + struct { + std::uint64_t before; + GuestItimerval timer; + std::uint64_t after; + } guarded; + std::memset(&guarded, 0xA5, sizeof(guarded)); + *__error_nid_postfix() = 12345; + Require(getitimer_nid_postfix(0, &guarded.timer) == 0, "getitimer failed"); + Require(*__error_nid_postfix() == 12345, "getitimer changed errno on success"); + Require(guarded.before == 0xA5A5A5A5A5A5A5A5ull && guarded.after == guarded.before, "getitimer wrote outside itimerval"); + Require(guarded.timer.interval.tv_sec == 0 && guarded.timer.interval.tv_usec == 0, "alarm acquired a repeat interval"); + Require(guarded.timer.value.tv_sec >= 0 && guarded.timer.value.tv_usec >= 0 + && guarded.timer.value.tv_usec < 1000000, "invalid timeval normalization"); + return guarded.timer.value.tv_sec * 1000000 + guarded.timer.value.tv_usec; +} + +int main() { + GuestAlarm_nid_no_patch(0); + Require(ReadRemaining() == 0, "inactive alarm is not zero"); + + for (int invalid : {-1, 3, std::numeric_limits::min(), std::numeric_limits::max()}) { + GuestItimerval output{{11, 22}, {33, 44}}; + const auto saved = output; + Require(getitimer_nid_postfix(invalid, &output) == -1 && *__error_nid_postfix() == 22, "invalid timer did not report EINVAL"); + Require(std::memcmp(&output, &saved, sizeof(output)) == 0, "invalid timer changed output"); + Require(getitimer_nid_postfix(invalid, nullptr) == -1 && *__error_nid_postfix() == 22, "invalid timer validation order changed"); + } + Require(getitimer_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == 14, "null output did not report EFAULT"); + for (int unsupported : {1, 2}) { + GuestItimerval output{{11, 22}, {33, 44}}; + const auto saved = output; + bool threw = false; + try { + getitimer_nid_postfix(unsupported, &output); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && std::memcmp(&output, &saved, sizeof(output)) == 0, "unsupported CPU timer reported fabricated state"); + } + + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); + Require(GuestAlarm_nid_no_patch(30) == 0, "alarm was already active"); + const auto first = ReadRemaining(); + Require(first > 0 && first <= 30000000, "armed alarm is not visible"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + Require(ReadRemaining() < first, "getitimer does not count down"); + std::array readers; + for (auto& reader : readers) reader = std::thread([] { + for (int i = 0; i < 100; ++i) Require(ReadRemaining() > 0, "reading consumed the alarm"); + }); + for (auto& reader : readers) reader.join(); + Require(GuestAlarm_nid_no_patch(0) > 0, "getitimer cancelled the alarm"); + Require(ReadRemaining() == 0, "cancelled alarm is not zero"); + + GuestAlarm_nid_no_patch(std::numeric_limits::max()); + Require(ReadRemaining() > static_cast(std::numeric_limits::max()) * 1000000, + "long alarm was truncated to signed 32 bits"); + GuestAlarm_nid_no_patch(1); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + Require(ReadRemaining() == 0, "expired alarm is not zero"); + GuestAlarm_nid_no_patch(0); +} From 81e3e34637bbd5005f8689f433e371cfbd74a12a Mon Sep 17 00:00:00 2001 From: Hannah Lyons <7751607+babyhuey@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:31:23 -0400 Subject: [PATCH 070/277] test(libScePosixForWebKit): pin fnmatch bracket class syntax to FreeBSD 11 --- core/libs/tests/GuestFnmatch.cpp | 4 ++++ docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFnmatch.cpp b/core/libs/tests/GuestFnmatch.cpp index 63b25c270..32b4e5b16 100644 --- a/core/libs/tests/GuestFnmatch.cpp +++ b/core/libs/tests/GuestFnmatch.cpp @@ -57,6 +57,10 @@ int main() { {"[!]x", "x", 0x00, 1}, {"*/", "dir/", 0x02, 0}, {"*/", "dir", 0x02, 1}, + {"[[:digit:]]", "7", 0x00, 1}, + {"[[:digit:]]", "d]", 0x00, 0}, + {"[[:digit:]]", ":]", 0x00, 0}, + {"a[[:alpha:]]", "ab", 0x00, 1}, }; for (const Case& entry : cases) { if (fnmatch_nid_postfix(entry.pattern, entry.string, entry.flags) != entry.result) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4155c8080..061af9eaa 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -350,7 +350,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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 from the same thread-local host `std::random_device` as `arc4random_buf`, 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. Multibyte characters and locale collation are not modelled. A null pattern or string throws +- [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 - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, 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. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` 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 afcaf91019b152e035589cdf7b1bd708808bffa3 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 13:32:20 +0000 Subject: [PATCH 071/277] test(agc): cover hand-built VGPR register offset --- .../prx/libSceAgc/tests/ShaderHelpers.cpp | 19 +++---------------- docs/dev/TechnicalDebt.md | 1 - 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index ce8451e0e..ad88c7a41 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -56,7 +56,7 @@ void HandBuiltUserVgprRegister() { handBuilt.ge_cntl = {GE_CNTL, 0x100u}; handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u}; handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2}; - handBuilt.ge_user_vgpr_en = {0x25cu, 0}; + handBuilt.ge_user_vgpr_en = {GE_USER_VGPR1, 0}; Shader preRaster{}; preRaster.type = static_cast(ShaderBinaryType::Gs); preRaster.specials = &handBuilt; @@ -65,8 +65,8 @@ void HandBuiltUserVgprRegister() { preparations = 0; Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0); Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast(GsOutputPrimitiveType::Triangles)); - Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == 0x25cu && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); - Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == 0x25cu && preparedPrimitive[1].value == 0); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == GE_USER_VGPR1 && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == GE_USER_VGPR1 && preparedPrimitive[1].value == 0); handBuilt.ge_cntl.offset = 0x25cu; std::string message; try { static_cast(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); } @@ -145,19 +145,6 @@ int main() { } } } - ShaderSpecialRegs disabledVgprSpecial = special; - disabledVgprSpecial.ge_user_vgpr_en = {GE_USER_VGPR1, 0}; - vertex.specials = &disabledVgprSpecial; - preparations = 0; - std::array normalizedPrimitive{}; - Require(sceAgcCreatePrimState(nullptr, normalizedPrimitive.data(), nullptr, &vertex, 7) == 0); - Require(preparations == 1 && preparedPrimitive[1].offset == GE_USER_VGPR_EN && preparedPrimitive[1].value == 0); - disabledVgprSpecial.ge_user_vgpr_en.value = 1; - bool rejectedEnabledVgprAlias = false; - try { static_cast(sceAgcCreatePrimState(nullptr, normalizedPrimitive.data(), nullptr, &vertex, 7)); } - catch (const std::runtime_error&) { rejectedEnabledVgprAlias = true; } - Require(rejectedEnabledVgprAlias); - vertex.specials = &special; preparations = 0; std::array patchContext{}; std::array patchPrimitive{}; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index bad8319a2..664e28ff2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -149,7 +149,6 @@ Throughout the project, every function at every stage either **does exactly what - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. -- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - a `ge_user_vgpr_en` special at `GE_USER_VGPR1` (0x25c) with value 0 is accepted and returned at `GE_USER_VGPR_EN` (0x262), as hand-built shader headers of PPSA99039 store it (issue #3457); the console's behaviour is not known. The same offset with a non-zero value throws - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws From 265c982eaa12cdc49f9c3be85eb4d519dbcdc32d Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 11 Oct 2026 15:33:55 +0200 Subject: [PATCH 072/277] refactor(agc): name zARR5aCmkoY sceAgcDcbAtomicGds_0900 --- core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp | 3 +-- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp index 8977432c4..eefbb3120 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -13,8 +13,7 @@ extern "C" { // unknown signature -APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY); -void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(void) { +void* APS5_VABI sceAgcDcbAtomicGds_0900(void) { NotImplemented_nid_no_patch(__func__); return nullptr; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..6e80ca7af 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -150,7 +150,7 @@ Throughout the project, every function at every stage either **does exactly what - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown -- [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature +- [sceAgcDcbAtomicGds_0900](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - name from its NID (`zARR5aCmkoY`), the DCB counterpart of `sceAgcAcbAtomicGds_0900`, signature unknown - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way From 3d839993503c0853f26eaaed21e00e8d495b1f0e Mon Sep 17 00:00:00 2001 From: marshmll Date: Sun, 11 Oct 2026 10:34:29 -0300 Subject: [PATCH 073/277] fix(build): require an SDL audio backend on Linux The bundled SDL builds its PipeWire, PulseAudio and ALSA drivers only when their development files are found, and otherwise keeps just the silent dummy and disk drivers without an error. The Linux CI and release jobs install none of them, so the v0.1.1 Linux libraries (libSceAudioOut, libSceAgcDriver, libfmod) contain no audible driver. Fail configuration on Linux when SDL found none of the three, install the Debian development packages in the Linux build and release jobs, and list the packages in the build instructions. --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- CMakeLists.txt | 10 ++++++++++ docs/dev/BUILD.md | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3e9c238d..724a3cfe0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -215,8 +215,8 @@ jobs: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - - name: Install lavapipe for the GPU execution tests - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers + - name: Install lavapipe for the GPU execution tests and the audio 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 - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 544be3ea4..f54726afa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,9 +60,9 @@ jobs: with: key: ${{ matrix.platform }} save: false - - name: Install lavapipe for the GPU execution tests + - 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 + 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 - name: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..ac7cf1f17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,16 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE) + get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO) + get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA) + if(NOT ANYPS5_SDL_PIPEWIRE AND NOT ANYPS5_SDL_PULSEAUDIO AND NOT ANYPS5_SDL_ALSA) + message(FATAL_ERROR "SDL found no PipeWire, PulseAudio or ALSA development files, so audio output would be silent. " + "Install libpipewire-0.3-dev, libpulse-dev and libasound2-dev (Debian, Ubuntu) " + "or pipewire-devel, pulseaudio-libs-devel and alsa-lib-devel (Fedora), then rerun configuration.") + endif() + endif() endif() set(ARCHITECTURE x86_64) add_subdirectory(3rdparty/ffmpeg-core) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 50e7023fe..29bfcddc6 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -27,7 +27,7 @@ git submodule update --init --recursive ## Requirements - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. -- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). +- 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. - 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 69e3c1d1154d2f3113c0c2fffde2c2f66280f10c Mon Sep 17 00:00:00 2001 From: Simon <340958984+ee6aba54@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:12:28 +0200 Subject: [PATCH 074/277] fix(libSceNpTrophy2): accept a null details or data in GetTrophyInfo and GetGroupInfo sceNpTrophy2GetTrophyInfo and sceNpTrophy2GetGroupInfo rejected a call unless both outputs were given, although both already fill each output only when it is not null. PPSA18391 (Clickteam Fusion) queries trophies with a null details. Only a call with neither output is rejected now, as in sceNpTrophy2GetGameInfo. --- .../prx/libSceNpTrophy2/src/GroupInfo.cpp | 2 +- .../prx/libSceNpTrophy2/src/TrophyInfo.cpp | 2 +- core/libs/tests/GuestNpTrophy2.cpp | 35 +++++++++++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp index 09fde57f5..5e97582aa 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index 0f19c9782..5090940b5 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 6a5111fc9..165d4347f 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI sceNpTrophy2RegisterUnlockCallback(void*, void*); int APS5_VABI sceNpTrophy2UnregisterUnlockCallback(); int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); int APS5_VABI sceNpTrophy2ShowTrophyList(int); } @@ -14,6 +16,15 @@ static void Require(bool value) { if (!value) std::abort(); } static void APS5_VABI OnUnlock(int, int, void*) {} +template static bool ThrowsInvalidArgument(Call call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + int main() { int userdata = 0; Require(sceNpTrophy2UnregisterUnlockCallback() == 0); @@ -29,11 +40,21 @@ int main() { Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); Require(details.num_trophies != 0); Require(sceNpTrophy2GetGameInfo(1, 1, nullptr, &data) == 0); - bool threw = false; - try { - sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); })); + + NpTrophy2GroupDetails groupDetails{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, &groupDetails, nullptr) == 0); + Require(groupDetails.group_id == 2); + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, &groupData) == 0); + Require(groupData.group_id == 2); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, nullptr); })); + + NpTrophy2Details trophyDetails{}; + NpTrophy2Data trophyData{}; + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, &trophyDetails, nullptr) == 0); + Require(trophyDetails.trophy_id == 3); + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, &trophyData) == 0); + Require(trophyData.trophy_id == 3); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, nullptr); })); } From 93f5220d1105933000e7cb372ee6c419ac05bbd3 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Sun, 11 Oct 2026 15:36:25 +0200 Subject: [PATCH 075/277] refactor(audioin): name X+4jdIS75P0 sceAudioInIsPadEmbeddedMic --- core/libs/prx/libSceAudioIn/Export.cpp | 4 ++-- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 50e917ee5..86fbd70d5 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -189,8 +189,8 @@ int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t i return 0; } -APS5_EXPORT("X+4jdIS75P0", sceAudioInUnknown_X4jdIS75P0); -int32_t APS5_VABI sceAudioInUnknown_X4jdIS75P0(void) { +int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { + (void)handle; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..af6c652bf 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -202,7 +202,7 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_getname_np](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature +- [sceAudioInIsPadEmbeddedMic](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted From 32a3047b5d918d71c75a42e117c5e91b54ece168 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Sun, 11 Oct 2026 04:19:43 +0300 Subject: [PATCH 076/277] fix(libkernel): run APR buffers past an unsatisfied wait on their own queue sceKernelAprSubmitCommandBuffer ran the whole buffer on the submitting thread, so a WaitOnAddress or WaitOnCounter that the submitter itself satisfies after the submit returned never returned. A buffer now runs on the submitting thread until such a wait; the rest of it, and every APR or AMM buffer submitted after it, runs in order on one APR thread. sceKernelAprWaitCommandBuffer and sceAmprAmmWaitCommandBufferCompletion wait for that completion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Wu3nuU87RJwjkkmxqft3sD --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 130 +++++++++++++++++++++--- core/libs/tests/GuestAmpr.cpp | 29 ++++++ 2 files changed, 147 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8aa37a332..1811d9b8a 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -1,3 +1,7 @@ +#include +#include +#include +#include #include #include #include "SceTypes.hpp" @@ -523,9 +527,13 @@ void _readResolved(Apr::Opcode opcode, Apr::ReadFileCommand command, ReadCursor& read = {true, command.fileId, command.destination + command.size, command.offset + command.size}; } -void _execute(const Apr::CommandBufferObject& buffer) { +struct Deferred { std::uint32_t cursor = 0; ReadCursor read; +}; + +std::optional _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { + std::uint32_t cursor = start; while (cursor < buffer.offset) { if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; @@ -604,6 +612,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { return std::atomic_ref(*reinterpret_cast(command.address)).load(std::memory_order_acquire); }; const std::uint64_t reference = (command.reference & command.mask) << unused; + if (defer && !_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) return Deferred{cursor, read}; while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) PreciseSleepUs(50); break; } @@ -630,6 +639,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { } cursor += header.bytes; } + return std::nullopt; } } @@ -658,16 +668,113 @@ void AmmVirtualAddressRanges_nid_no_patch(std::uint64_t* start, std::uint64_t* e if (multimapEnd) *multimapEnd = base + 2 * AmmRangeBytes; } +struct AprQueue { + std::mutex submit; + std::mutex mutex; + std::condition_variable ready; + std::condition_variable done; + struct Work { + std::vector commands; + Deferred resume; + std::uint32_t aprId = 0; + std::uint32_t ammId = 0; + }; + std::deque pending; + bool running = false; + std::uint32_t aprSubmitted = 0; + std::uint32_t aprCompleted = 0; + std::uint32_t ammSubmitted = 0; + std::uint32_t ammCompleted = 0; +}; + +void _complete(AprQueue& queue, std::uint32_t aprId, std::uint32_t ammId) { + if (aprId != 0) queue.aprCompleted = std::max(queue.aprCompleted, aprId); + if (ammId != 0) queue.ammCompleted = std::max(queue.ammCompleted, ammId); +} + +AprQueue& _queue() { + static AprQueue* queue = [] { + auto* created = new AprQueue(); + std::thread([created] { + for (;;) { + AprQueue::Work work; + { + std::unique_lock lock(created->mutex); + created->ready.wait(lock, [&] { return !created->pending.empty(); }); + work = std::move(created->pending.front()); + created->pending.pop_front(); + created->running = true; + } + try { + const auto bytes = static_cast(work.commands.size()); + _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, work.commands.data()}, work.resume.cursor, work.resume.read); + } catch (const std::exception& error) { + std::fprintf(stderr, "APR: %s\n", error.what()); + std::fflush(stderr); + std::abort(); + } + { + std::lock_guard lock(created->mutex); + created->running = false; + _complete(*created, work.aprId, work.ammId); + } + created->done.notify_all(); + } + }).detach(); + return created; + }(); + return *queue; +} + +std::uint32_t _submit(const std::uint8_t* base, std::uint32_t bytes, bool amm) { + auto& queue = _queue(); + std::lock_guard submitLock(queue.submit); + std::uint32_t id; + bool queued; + { + std::lock_guard lock(queue.mutex); + id = amm ? ++queue.ammSubmitted : ++queue.aprSubmitted; + queued = queue.running || !queue.pending.empty(); + } + const auto enqueue = [&](Deferred resume) { + { + std::lock_guard lock(queue.mutex); + queue.pending.push_back({std::vector(base, base + bytes), resume, amm ? 0u : id, amm ? id : 0u}); + } + queue.ready.notify_one(); + }; + if (queued) { + enqueue({}); + return id; + } + if (const auto deferred = _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, const_cast(base)}, 0, {}, true)) { + enqueue(*deferred); + return id; + } + { + std::lock_guard lock(queue.mutex); + _complete(queue, amm ? 0u : id, amm ? id : 0u); + } + queue.done.notify_all(); + return id; +} + +void _waitSubmitted(std::uint32_t id, bool amm) { + auto& queue = _queue(); + std::unique_lock lock(queue.mutex); + queue.done.wait(lock, [&] { return amm ? queue.ammCompleted >= id || id > queue.ammSubmitted : queue.aprCompleted >= id || id > queue.aprSubmitted; }); +} + std::uint32_t AmmSubmit_nid_no_patch(void* base, std::uint32_t bytes) { - _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, static_cast(base)}); - auto& last = _amm().lastSubmit; - std::uint32_t id = last.fetch_add(1) + 1; - while (id == 0) id = last.fetch_add(1) + 1; + const auto id = _submit(static_cast(base), bytes, true); + _amm().lastSubmit.store(id); return id; } bool AmmSubmitted_nid_no_patch(std::uint32_t id) { - return id != 0 && id <= _amm().lastSubmit.load(); + if (id == 0 || id > _amm().lastSubmit.load()) return false; + _waitSubmitted(id, true); + return true; } @@ -729,16 +836,15 @@ int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* buffer, uint32_t priority) { (void)priority; if (!buffer) return _fail(GUEST_EINVAL); - _execute(*buffer); + _submit(buffer->base, buffer->offset, false); return 0; } int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { if (!id) return _fail(GUEST_EINVAL); - const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); - if (result != 0) return result; - static std::atomic nextId{1}; - *id = nextId.fetch_add(1); + (void)priority; + if (!buffer) return _fail(GUEST_EINVAL); + *id = _submit(buffer->base, buffer->offset, false); return 0; } @@ -751,7 +857,7 @@ int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult(const Apr::CommandBuff } int APS5_VABI sceKernelAprWaitCommandBuffer(uint32_t id) { - (void)id; + _waitSubmitted(id, false); return 0; } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 43b223fac..c4820323e 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include +#include #include #include #include @@ -33,6 +34,8 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char*, std::uint32_t); int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceKernelAprWaitCommandBuffer(std::uint32_t); int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWaitOnCounter(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteCounterOnCompletion(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t); @@ -301,6 +304,31 @@ void TestWaits() { Require(submitted.get() == 0 && done == 1); } +void TestWaitSatisfiedAfterSubmission() { + Recorder first; + Recorder second; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + alignas(8) std::uint64_t later = 0; + Require(sceAmprCommandBufferWaitOnAddress(&first.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&first.buffer, &done, 1) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&second.buffer, &later, 2) == 0); + std::uint32_t firstId = 0; + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBufferAndGetId(&first.buffer, 0, &firstId); }); + const bool returned = submitted.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(submitted.get() == 0 && firstId != 0); + std::uint32_t secondId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&second.buffer, 0, &secondId) == 0 && secondId > firstId); + Require(std::atomic_ref(done).load(std::memory_order_acquire) == 0); + Require(std::atomic_ref(later).load(std::memory_order_acquire) == 0); + std::atomic_ref(flag).store(1, std::memory_order_release); + Require(sceKernelAprWaitCommandBuffer(secondId) == 0); + Require(done == 1 && later == 2); + Require(sceKernelAprWaitCommandBuffer(firstId) == 0); +} + void TestCounters() { Recorder recorder; alignas(8) std::uint64_t single = 0; @@ -1045,6 +1073,7 @@ int main() { TestSubmission(); TestKernelEventQueue(); TestWaits(); + TestWaitSatisfiedAfterSubmission(); TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); From 6ef110af48e6d909dab7c487593533534cfece20 Mon Sep 17 00:00:00 2001 From: DariusHQ Date: Sun, 11 Oct 2026 16:41:36 +0300 Subject: [PATCH 077/277] feat(libc): implement wcstok PS5MC (PPSA99420) stops at start-up on wcstok once sendfile is in. It follows FreeBSD's three-argument wcstok over the guest's 16-bit wchar_t: leading delimiters are skipped, the token ends at the next delimiter, which is overwritten with 0, and *last is set to null when the string runs out. --- core/libs/prx/libc/src/Strings.cpp | 19 +++++++++++++++++ core/libs/tests/GuestWideStrings.cpp | 32 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 978a499aa..90fd9bf7c 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -427,6 +427,25 @@ size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept) { return count; } +char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last) { + if (text == nullptr) text = *last; + if (text == nullptr) return nullptr; + while (*text != 0 && Contains(delimiters, *text)) ++text; + if (*text == 0) { + *last = nullptr; + return nullptr; + } + char16_t* end = text; + while (*end != 0 && !Contains(delimiters, *end)) ++end; + if (*end == 0) { + *last = nullptr; + } else { + *end = 0; + *last = end + 1; + } + return text; +} + char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { for (size_t index = 0; index < n; ++index) s[index] = c; return s; diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 2e01b4d33..64dd0afc3 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -19,6 +19,7 @@ const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsstr_nid_postfix(const char16_t* haystack, const char16_t* needle); const char16_t* APS5_VABI wcspbrk_nid_postfix(const char16_t* s, const char16_t* accept); std::size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept); +char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last); char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, std::size_t n); double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr); float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr); @@ -96,6 +97,37 @@ int main() { require(wcsspn_nid_postfix(u"aabbc", u"ab") == 4); require(wcsspn_nid_postfix(u"abc", u"") == 0); + char16_t tokens[] = u",,one,,two;three,"; + char16_t* last = tokens; + char16_t* token = wcstok_nid_postfix(tokens, u",", &last); + require(token == tokens + 2 && same(token, u"one", 4) && last == tokens + 6); + token = wcstok_nid_postfix(nullptr, u",;", &last); + require(token == tokens + 7 && same(token, u"two", 4) && last == tokens + 11); + token = wcstok_nid_postfix(nullptr, u",", &last); + require(token == tokens + 11 && same(token, u"three", 6) && last == tokens + 17); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + + char16_t tail[] = u"a b"; + require(wcstok_nid_postfix(tail, u" ", &last) == tail && last == tail + 2); + require(wcstok_nid_postfix(nullptr, u" ", &last) == tail + 2 && same(tail, u"a\0b", 4) && last == nullptr); + + char16_t onlyDelimiters[] = u";;"; + last = onlyDelimiters; + require(wcstok_nid_postfix(onlyDelimiters, u";", &last) == nullptr && last == nullptr && same(onlyDelimiters, u";;", 3)); + + char16_t empty[] = u""; + last = empty; + require(wcstok_nid_postfix(empty, u";", &last) == nullptr && last == nullptr); + + char16_t whole[] = u"word"; + require(wcstok_nid_postfix(whole, u"", &last) == whole && last == nullptr && same(whole, u"word", 5)); + + char16_t wide[] = u"\u4e00\u3001\xd83d\xde00\u3001"; + require(wcstok_nid_postfix(wide, u"\u3001", &last) == wide && same(wide, u"\u4e00\0", 2) && last == wide + 2); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == wide + 2 && wide[2] == 0xd83d && wide[3] == 0xde00 && wide[4] == 0 && last == wide + 5); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == nullptr && last == nullptr); + const char16_t units[] = {u'x', 0, u'ï¿¿', u'y'}; require(wmemchr_nid_postfix(units, u'ï¿¿', 4) == units + 2); require(wmemchr_nid_postfix(units, u'y', 3) == nullptr); From c3db0b0d158d81898bf308a3a9451694445cad9d Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:19:14 +0530 Subject: [PATCH 078/277] perf(libSceAgcDriver): cap draw input snapshot allocation size In #3338, Ratchet & Clank: Rift Apart (PPSA01474) hits a 36 GB / 5 s memcpy stall at frame ~380 right after intro logos because the GPU worker thread is blocked copying multi-gigabyte storage buffers on every draw snapshot miss (specifically a 3.01 GiB streaming range 0x118f800000+0xbc200000 and a 232 MiB range 0x1171258040+0xe7fffc0 across 130,000 draws). ShaderResources::PrepareDrawBindings now caps both per-allocation and cumulative draw input buffer snapshot copies at 64 MiB by default, overridable via APS5_DRAW_SNAPSHOT_MAX_MIB. Inputs exceeding the threshold skip snapshotting and bind directly in place, preventing multi-gigabyte memory copies while preserving standard small uniforms and draw arguments. Budget allocation prioritises earlier descriptor bindings in deterministic pipeline layout order. Adds drawSnapshotCapTests in core/libs/prx/libSceAgcDriver/tests/Recorder.cpp verifying individual and cumulative capping, cross-platform cache behavior on watched and compared hosts, and direct observation of subsequent CPU writes on in-place bound ranges exceeding the cap. Exercised via --draw-snapshot-cap-only registered as agc_draw_snapshot_cap in libSceAgcDriver/CMakeLists.txt. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 10 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 150 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 165 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..d3bacf241 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -786,8 +786,10 @@ if(BUILD_TESTING) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") 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) + set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index dbfdf1581..9c66fb62a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -136,6 +136,7 @@ public: }; std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..0882656ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3374,9 +3374,17 @@ std::optional> ShaderResources::MovedR return moved; } +std::size_t ShaderResources::MaxDrawSnapshotBytes() { + const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull; + return static_cast(mib * 1024ull * 1024ull); +} + std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if (_set == VK_NULL_HANDLE || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); + const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); + std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; for (std::size_t index = 0; index < allocations.size(); ++index) { @@ -3404,6 +3412,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } const auto begin = address - item.adjustment; const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); + if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const auto generation = GuestMemory::CollectWrites(begin, bytes); auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); @@ -3414,6 +3423,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } selected.push_back(index); result->snapshots.push_back({begin, std::move(buffer)}); + totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } if (selected.empty()) return {}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a67d7e6c..0c7912500 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1764,6 +1764,150 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotCapTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + const char* previous = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const std::string saved = previous != nullptr ? previous : ""; +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot set snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot set snapshot cap"); +#endif + struct RestoreCap { + const char* previous; + std::string saved; + ~RestoreCap() { +#ifdef _WIN32 + _putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str()); +#else + if (previous != nullptr) { + setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str(), 1); + } else { + unsetenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + } +#endif + } + } restore{previous, saved}; + constexpr std::size_t bytes = 3 * 1024 * 1024; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot cap not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot cap not tested\n"; + return; + } + const auto smallAddress = address + 4096; + constexpr std::size_t smallBytes = 1024 * 1024 - 4096; + const auto largeAddress = address + 1024 * 1024 + 8192; + constexpr std::size_t largeBytes = 1024 * 1024 + 4096; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding smallBinding; + smallBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + smallBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + smallBinding.descriptorSet = 0; + smallBinding.binding = 0; + smallBinding.count = 1; + smallBinding.guestDescriptor = {static_cast(smallAddress), static_cast(smallAddress >> 32u) & 0xffffu, static_cast(smallBytes), 0x31000000u}; + smallBinding.bufferWritten = {false}; + program.bindings.push_back(smallBinding); + ShaderRecompiler::DescriptorBinding largeBinding; + largeBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + largeBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + largeBinding.descriptorSet = 0; + largeBinding.binding = 1; + largeBinding.count = 1; + largeBinding.guestDescriptor = {static_cast(largeAddress), static_cast(largeAddress >> 32u) & 0xffffu, static_cast(largeBytes), 0x31000000u}; + largeBinding.bufferWritten = {false}; + program.bindings.push_back(largeBinding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared"); + Require(bindings->snapshots.size() == 1, "an input exceeding the draw snapshot cap was snapshotted"); + Require(bindings->snapshots[0].address == smallAddress, "the snapshotted input does not match the smaller buffer below the cap"); + Require(snapshotRecorder.ReusableDrawSnapshot(largeAddress, largeBytes) == nullptr, "an input exceeding the draw snapshot cap was kept in the snapshot cache"); + if (Watched(address, bytes)) { + Require(snapshotRecorder.ReusableDrawSnapshot(smallAddress, smallBytes) != nullptr, "an input below the draw snapshot cap was not kept in the snapshot cache"); + } + const auto contents = bindings->snapshots[0].buffer->Bytes(); + Require(contents.size() == smallBytes && std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "a draw snapshot does not hold the guest bytes of its draw"); + std::memset(reinterpret_cast(smallAddress), 0x22, smallBytes); + std::memset(reinterpret_cast(largeAddress), 0x33, largeBytes); + Require(std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "snapshotted input observed CPU overwrite"); + const auto* inPlace = reinterpret_cast(largeAddress); + 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(); + } + const auto firstAddress = address + 4096; + constexpr std::size_t firstBytes = 600 * 1024; + const auto secondAddress = address + 600 * 1024 + 8192; + constexpr std::size_t secondBytes = 600 * 1024; + ShaderRecompiler::RecompileResult cumulativeProgram; + ShaderRecompiler::DescriptorBinding firstBinding; + firstBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + firstBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + firstBinding.descriptorSet = 0; + firstBinding.binding = 0; + firstBinding.count = 1; + firstBinding.guestDescriptor = {static_cast(firstAddress), static_cast(firstAddress >> 32u) & 0xffffu, static_cast(firstBytes), 0x31000000u}; + firstBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(firstBinding); + ShaderRecompiler::DescriptorBinding secondBinding; + secondBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + secondBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + secondBinding.descriptorSet = 0; + secondBinding.binding = 1; + secondBinding.count = 1; + secondBinding.guestDescriptor = {static_cast(secondAddress), static_cast(secondAddress >> 32u) & 0xffffu, static_cast(secondBytes), 0x31000000u}; + secondBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(secondBinding); + const CompiledShader cumulativeCompute{ShaderRecompiler::ShaderStage::Compute, &cumulativeProgram, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, cumulativeCompute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared for cumulative test"); + Require(bindings->snapshots.size() == 1, "cumulative cap permitted both inputs to snapshot"); + Require(bindings->snapshots[0].address == firstAddress, "the earlier binding in the layout was not prioritised for the snapshot budget"); + snapshotRecorder.Sync(); + } + recorder.Activate(); +} void drawSnapshotEvictionTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -4979,6 +5123,11 @@ int main(int argc, char** argv) { std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { + drawSnapshotCapTests(device, recorder); + std::cout << "Draw snapshot cap tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); @@ -5001,6 +5150,7 @@ int main(int argc, char** argv) { misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..bfdadaad4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -370,6 +370,7 @@ Throughout the project, every function at every stage either **does exactly what - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [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`. - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From eba67d4130984a0d380a184dd6249caac423c991 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sun, 11 Oct 2026 15:44:20 +0200 Subject: [PATCH 079/277] fix(agc): render a slice of a 2D array color target into that layer of the array A color view of slice N of a 2D array (CB_COLOR_VIEW SLICE_START, with CB_COLOR_ATTRIB3 MIP0_DEPTH as the last slice) was rendered as a 2D surface at the slice's address. The swizzle equations of the XOR tile modes take the slice index (the detiler's slice bits), so the texels of slice N of an array are not where a 2D surface at that address puts them: a title that renders into array slices and then samples the array read them scrambled. The decoded target now keeps the array's address, its last slice and the view's slice. A resident target that is a slice of an array attaches that layer of the array's storage image, so the texels follow the array's layout; storage images of 2D arrays are color attachments for this, and an attachment view selects a layer. A slice whose array has DCC, CMASK or an attachment proxy has no resident array image, and a non-resident slice of an XOR-tiled array now throws instead of being written in the 2D layout. --- .../Graphics/include/State.hpp | 3 + .../Graphics/include/Texture.hpp | 4 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 22 +- .../libSceAgcDriver/Graphics/src/State.cpp | 3 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 11 +- .../tests/execution/ArrayLayerTarget.cpp | 289 ++++++++++++++++++ 6 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index cd0d2c3b2..0012dbfa5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -54,6 +54,9 @@ struct ColorTarget { std::uint32_t slot = 0; std::uint32_t depth = 1; std::uint32_t depthSlice = 0; + std::uint64_t arrayAddress = 0; + std::uint32_t arrayLastSlice = 0; + std::uint32_t arraySlice = 0; std::uint32_t exportIndex = 0; bool uintExport = false; std::uint32_t pipeBankXor = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9f4c7765a..5e71b21d6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -159,7 +159,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0); VkImageView AttachmentProxyView(); void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; @@ -529,7 +529,7 @@ private: std::map elementLayerViews; std::int8_t singlePassState = -1; bool attachable = false; - std::map, VkImageView> attachmentViews; + std::map, VkImageView> attachmentViews; VkImage proxyImage = VK_NULL_HANDLE; VkDeviceMemory proxyMemory = VK_NULL_HANDLE; VkImageView proxyView = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adb76dd93..b7454391d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -87,6 +87,17 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } +GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { + Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array"); + auto surface = SurfaceForTarget(color); + surface.baseAddress = color.arrayAddress; + surface.width = color.surfaceExtent.width; + surface.height = color.surfaceExtent.height; + surface.depthOrLastArray = color.arrayLastSlice; + surface.dimension = TextureDimension::k2DArray; + return surface; +} + } namespace { @@ -1882,6 +1893,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // APS5_PACKET_EPOCH=1); the key scan runs on every lookup regardless. Lookups of a // millisecond or more are counted apart; the [texture] line says how many uploaded. binding.resident = refreshResidentTarget(context, state, color, outcome, profile, [&] { + if (color.arrayLastSlice != 0) { + Require(color.dccAddress == 0 && color.cmaskAddress == 0 && AttachmentProxyFormat(context, color.format) == VK_FORMAT_UNDEFINED, "a slice of a 2D array color target with DCC, CMASK or an attachment proxy has no resident array image"); + const auto array = ArraySurfaceForTarget(color); + Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * DescribeSurface(array).layerBytes, "the array image's layer pitch differs from the color view's slice offset"); + auto resident = CachedStorageSurface(context, array); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + return resident; + } auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); @@ -1891,12 +1910,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED; - targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice)); + targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice)); continue; } materializeCmaskClear(context, color, nullptr); Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); + Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { binding.mip = ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cf0d6b1ea..66eb91ed0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -872,6 +872,8 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.depthSlice = slice; } else if ((attrib3 & 0x1fffu) != 0) { Require(slice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface"); + color.arrayLastSlice = attrib3 & 0x1fffu; + color.arraySlice = slice; } color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; @@ -895,6 +897,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.pipeBankXor = static_cast(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u)); color.surfaceAddress -= color.pipeBankXor; } + color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0; if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7a21d9720..e9558ef4c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -909,7 +909,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, { VkFormatProperties properties{}; context.formatProperties(context.physical, vkFormat, &properties); - attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; + attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D || descriptor.dimension == TextureDimension::k2DArray) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; if (attachable) imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; } imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1317,12 +1317,13 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) { Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); - const auto found = attachmentViews.find({format, mip, depthSlice}); + Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); + const auto found = attachmentViews.find({format, mip, depthSlice, layer}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -1332,10 +1333,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s 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, mip, 1u, volume ? depthSlice : descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(std::tuple{format, mip, depthSlice}, created); + attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created); return created; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp new file mode 100644 index 000000000..53887c1b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -0,0 +1,289 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.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 + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureDimension; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Layers = 4; +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 Type2DArray = 13; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; +constexpr std::uint32_t ScissorWidth = 16; +constexpr float Drawn = 0.25f; + +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, Layers> LoadCodes{}; +alignas(256) std::array PixelCode{}; +alignas(256) std::array Output{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + LayerBytes - 1) & ~(static_cast(LayerBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + LayerBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "array layer target: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array ArrayDescriptor(std::uint64_t address) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2DArray << 28u), + Layers - 1u, + 0u, + 0u, + 0u, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const std::vector& 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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer) { + 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); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(device, code, userData); +} + +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; + Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected)); + } +} + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = slice | (slice << 13u); + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3 | (Layers - 1u); + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); + Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + return color; +} + +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelCode[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelCode[at++] = std::bit_cast(Drawn); + } + PixelCode[at++] = 0xf800180fu; + PixelCode[at++] = 0x07060504u; + PixelCode[at++] = 0xbf810000u; + const std::span pixelCode(PixelCode.data(), at); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const auto color = DecodeTarget(surface, slice); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + 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 = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)), 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); + device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto drawn = std::bit_cast(Drawn); + DrawSlice(device, surface, 1); + const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); + LoadLayer(device, surface, 1); + RequireOutput(drawn, Initial, "slice 1 read through the array after the draw"); + for (const std::uint32_t other : {0u, 2u, 3u}) { + LoadLayer(device, surface, other); + RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1"); + } + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + LoadLayer(device, surface, 1); + RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + std::puts("array layer target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 07297a54aae24d74ea84ed964b13ec1c9fe88559 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:21:07 -0400 Subject: [PATCH 080/277] test(agc): verify rejected resource sets release storage images --- .../tests/execution/SampledCacheBudget.cpp | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 8fba8966d..0cf28122d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -241,6 +241,7 @@ bool resourceCacheTests(Context context, const std::string& mode) { context.textureCache = &textures; Block block(context); if (context.hostImportAlignment == 0 || HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "resource cache tests require host imports"); std::puts("skipped, resource cache tests require host imports"); return false; } @@ -268,30 +269,51 @@ bool resourceCacheTests(Context context, const std::string& mode) { for (const bool sampledImages : {false, true}) { const std::weak_ptr early = CachedStorageSurface(context, Resource(block.Surface(0))); auto oversized = ImageResources(context, block, 0, static_cast(Surfaces), sampledImages); - const auto image = early.lock(); + const std::weak_ptr rejected = oversized; + auto image = early.lock(); + Require(oversized->Reusable(), "the oversized descriptor set is not eligible for resource caching"); Require(image != nullptr && !image->Cached(), "the oversized descriptor set did not evict an early image while building"); - cache.Insert({1}, oversized); - Require(cache.Size() == 0, sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); + if (sampledImages) Require(oversized->SampledTextures().size() == Surfaces && oversized->SampledTextures().front()->StorageSource() == image.get(), "the oversized sampled descriptor set does not retain the early storage image"); + std::vector> displaced; + cache.Insert({1}, oversized, &displaced); + Require(cache.Size() == 0 && displaced.empty(), sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); + image.reset(); oversized.reset(); + TextureCacheUsage(); + Require(rejected.expired() && early.expired(), "the rejected oversized descriptor set retained an evicted storage image"); + std::puts(sampledImages ? "resource cache oversized sampled lifetime passed" : "resource cache oversized storage lifetime passed"); ClearCachedTextures(context.device); } } else { auto storage = ImageResources(context, block, 0, 1, false); auto sampled = ImageResources(context, block, 0, 1, true); + Require(storage->Reusable() && sampled->Reusable(), "the stale descriptor sets are not eligible for resource caching"); Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); - const auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr staleImage = image; + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; Require(image != nullptr && image->Cached(), "the resource has no cached storage image"); for (std::size_t surface = 1; surface < Surfaces && image->Cached(); ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); Require(!image->Cached(), "the test never evicted the oldest storage image"); auto live = ImageResources(context, block, Surfaces - 1, 1, false); cache.Insert({1}, live); Require(cache.Find({1}) == live, "the resource cache rejected a live descriptor set"); + std::vector> displaced; for (const auto& stale : {storage, sampled}) { - cache.Insert({2}, stale); - Require(cache.Find({2}) == nullptr && cache.Size() == 1, "an uncached image was admitted to the resource cache"); - cache.Insert({1}, stale); - Require(cache.Find({1}) == live && cache.Size() == 1, "an uncached image replaced a live resource-cache entry"); + cache.Insert({2}, stale, &displaced); + Require(cache.Find({2}) == nullptr && cache.Size() == 1 && displaced.empty(), "an uncached image was admitted to the resource cache"); + cache.Insert({1}, stale, &displaced); + Require(cache.Find({1}) == live && cache.Size() == 1 && displaced.empty(), "an uncached image replaced a live resource-cache entry"); } + image.reset(); + storage.reset(); + Require(storageSet.expired() && !sampledSet.expired() && !staleImage.expired(), "stale storage release did not preserve the sampled caller's image"); + std::puts("resource cache stale storage caller release passed"); + sampled.reset(); + TextureCacheUsage(); + Require(sampledSet.expired() && staleImage.expired(), "the rejected descriptor sets retained an evicted storage image"); + std::puts("resource cache stale sampled caller release passed"); } } catch (...) { cache.Clear(); From 2eed99e7702a08cf89ee58613eaf4937bef5d913 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:47:50 +0400 Subject: [PATCH 081/277] fix(relinker): validate ELF section tables and file mappings --- core/relinker/CMakeLists.txt | 10 + .../relinker/src/parsing/ElfReader.cpp | 64 +++-- .../relinker/tests/ElfReaderBoundsTests.cpp | 229 ++++++++++++++++++ 3 files changed, 278 insertions(+), 25 deletions(-) create mode 100644 core/relinker/relinker/tests/ElfReaderBoundsTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..309f267f2 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -137,6 +137,16 @@ endif() if(BUILD_TESTING) + add_test_executable(elf_reader_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ElfReaderBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp) + target_include_directories(elf_reader_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include) + add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) + set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index a92ef6238..c6af1ec10 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include namespace Relinker { @@ -132,6 +133,16 @@ std::vector ElfReader::ReadProgramHeaders() const { std::vector ElfReader::ReadSectionHeaders() const { const ElfHeader header = ReadHeader(); + if (header.SectionHeaderCount != 0 && header.SectionHeaderEntrySize != kShdrEntrySize) { + throw RelinkerException("Invalid ELF section header entry size: expected 64 bytes", 0x3a); + } + const ByteCount tableSize = static_cast(header.SectionHeaderCount) * header.SectionHeaderEntrySize; + if (!_rangeFits(header.SectionHeaderOffset, tableSize, _fileBuffer.size())) { + throw RelinkerException("Section header table out of bounds", header.SectionHeaderOffset); + } + if (header.SectionHeaderStringIndex != 0 && header.SectionHeaderStringIndex >= header.SectionHeaderCount) { + throw RelinkerException("Section header string table index out of bounds", 0x3e); + } std::vector headers; FileByteOffset offset = header.SectionHeaderOffset; @@ -146,7 +157,7 @@ std::vector ElfReader::ReadSectionHeaders() const { sh.SectionSize = _readU64At(offset + 0x20); sh.Link = _readU32At(offset + 0x28); sh.Info = _readU32At(offset + 0x2c); - sh.EntrySize = _readU64At(offset + 0x30); + sh.EntrySize = _readU64At(offset + kShdrEntSizeOffset); sh.Name = _resolveShdrName(nameOffset, header); @@ -165,17 +176,27 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade FileByteOffset shstrOffset = header.SectionHeaderOffset + (header.SectionHeaderStringIndex * header.SectionHeaderEntrySize); - const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); - - std::string name; - FileByteOffset currentPos = strTableOffset + nameOffset; - - while (currentPos < _fileBuffer.size() && _fileBuffer[currentPos] != '\0') { - name += static_cast(_fileBuffer[currentPos]); - currentPos++; + if (_readU32At(shstrOffset + kShdrTypeOffset) != SHT_STRTAB) { + throw RelinkerException("Section names do not reference a string table", shstrOffset); } - - return name; + const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); + const ByteCount strTableSize = _readU64At(shstrOffset + kShdrSizeOffset); + if (!_rangeFits(strTableOffset, strTableSize, _fileBuffer.size())) { + throw RelinkerException("Section name string table out of bounds", strTableOffset); + } + if (strTableSize == 0 && nameOffset == 0) { + return ""; + } + if (nameOffset >= strTableSize) { + throw RelinkerException("Section name offset out of bounds", nameOffset); + } + const auto first = _fileBuffer.begin() + strTableOffset + nameOffset; + const auto end = _fileBuffer.begin() + strTableOffset + strTableSize; + const auto terminator = std::find(first, end, 0); + if (terminator == end) { + throw RelinkerException("Unterminated section name", strTableOffset + nameOffset); + } + return std::string(first, terminator); } std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const { @@ -207,21 +228,14 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH } FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { - const ElfHeader header = ReadHeader(); - - FileByteOffset offset = header.ProgramHeaderOffset; - - for (std::uint16_t i = 0; i < header.ProgramHeaderCount; ++i) { - const std::uint32_t type = _readU32At(offset); - const FileByteOffset segOffset = _readU64At(offset + 0x08); - const VirtualAddress segVAddr = _readU64At(offset + 0x10); - const ByteCount segFileSize = _readU64At(offset + 0x20); - - if (type == PT_LOAD && address >= segVAddr && address < segVAddr + segFileSize) { - return segOffset + (address - segVAddr); + for (const auto& segment : ReadProgramHeaders()) { + if (segment.Type == PT_LOAD && address >= segment.MappedAddress && + address - segment.MappedAddress < segment.FileSize) { + if (!_rangeFits(segment.Offset, segment.FileSize, _fileBuffer.size())) { + throw RelinkerException("PT_LOAD file mapping out of bounds", segment.Offset); + } + return segment.Offset + (address - segment.MappedAddress); } - - offset += header.ProgramHeaderEntrySize; } throw RelinkerException("Virtual address not mapped by any PT_LOAD segment", address); diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp new file mode 100644 index 000000000..065688d6d --- /dev/null +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Invalid fixture offset"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +Bytes Fixture() { + Bytes bytes(0x300); + const std::uint8_t magic[] = {0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(bytes.data(), magic, sizeof(magic)); + Write(bytes, 16, 3); + Write(bytes, 18, 62); + Write(bytes, 20, 1); + Write(bytes, 32, 64); + Write(bytes, 40, 128); + Write(bytes, 52, 64); + Write(bytes, 54, 56); + Write(bytes, 56, 1); + Write(bytes, 58, 64); + Write(bytes, 60, 3); + Write(bytes, 62, 2); + Write(bytes, 64, 1); + Write(bytes, 72, 0x240); + Write(bytes, 80, 0x1000); + Write(bytes, 96, 32); + Write(bytes, 104, 64); + Write(bytes, 192, 1); + Write(bytes, 196, 1); + Write(bytes, 216, 0x240); + Write(bytes, 224, 32); + Write(bytes, 240, 8); + Write(bytes, 248, 24); + Write(bytes, 256, 7); + Write(bytes, 260, 3); + Write(bytes, 280, 0x200); + const char strings[] = "\0.text\0.shstrtab"; + Write(bytes, 288, sizeof(strings)); + std::memcpy(bytes.data() + 0x200, strings, sizeof(strings)); + return bytes; +} + +void Require(const bool condition) { + if (!condition) throw std::runtime_error("Unexpected parsed value"); +} + +void RejectSections(Bytes bytes) { + try { + Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid section metadata was accepted"); +} + +void RejectAddress(Bytes bytes, const std::uint64_t address) { + try { + Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(address); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid file mapping was accepted"); +} + +} + +int main() { + const std::vector>> cases = { + {"section entry size field", [] { + const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); + }}, + {"section header stride", [] { + for (const std::uint16_t stride : {0, 56, 65}) { + auto bytes = Fixture(); + Write(bytes, 58, stride); + RejectSections(std::move(bytes)); + } + }}, + {"section name table index", [] { + auto bytes = Fixture(); + Write(bytes, 62, 3); + RejectSections(std::move(bytes)); + }}, + {"section name table type", [] { + auto bytes = Fixture(); + Write(bytes, 260, 1); + RejectSections(std::move(bytes)); + }}, + {"section name table range", [] { + auto bytes = Fixture(); + Write(bytes, 288, 0x200); + RejectSections(std::move(bytes)); + }}, + {"section name offset", [] { + auto bytes = Fixture(); + Write(bytes, 192, 64); + bytes[0x240] = 'X'; + RejectSections(std::move(bytes)); + }}, + {"section name terminator", [] { + auto bytes = Fixture(); + Write(bytes, 288, 5); + RejectSections(std::move(bytes)); + }}, + {"section header table truncation", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + bytes.resize(128 + 3 * 64 - 1); + RejectSections(std::move(bytes)); + }}, + {"translation program header stride", [] { + for (const std::uint16_t stride : {0, 48, 64}) { + auto bytes = Fixture(); + Write(bytes, 54, stride); + RejectAddress(std::move(bytes), 0x1008); + } + }}, + {"translation virtual boundary", [] { + auto bytes = Fixture(); + const auto start = std::numeric_limits::max() - 31; + Write(bytes, 80, start); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(start + 16) == 0x250); + }}, + {"translation overflowing file offset", [] { + auto bytes = Fixture(); + Write(bytes, 72, std::numeric_limits::max() - 3); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation file mapping range", [] { + auto bytes = Fixture(); + Write(bytes, 72, 0x2f0); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"empty and absent name tables", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + const auto unnamed = Relinker::ElfReader(bytes).ReadSectionHeaders(); + Require(unnamed.size() == 3 && unnamed[1].Name.empty()); + Write(bytes, 62, 2); + Write(bytes, 288, 0); + Write(bytes, 192, 0); + Write(bytes, 256, 0); + const auto empty = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(empty.size() == 3 && empty[1].Name.empty()); + }}, + {"section name at table boundary", [] { + auto bytes = Fixture(); + Write(bytes, 192, 17); + RejectSections(std::move(bytes)); + }}, + {"section name table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 280, std::numeric_limits::max() - 3); + RejectSections(std::move(bytes)); + }}, + {"section table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + Write(bytes, 40, std::numeric_limits::max() - 31); + RejectSections(std::move(bytes)); + }}, + {"valid string table suffix and final null", [] { + auto bytes = Fixture(); + Write(bytes, 192, 2); + Write(bytes, 256, 16); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections[1].Name == "text" && sections[2].Name.empty()); + }}, + {"ELF without section or program tables", [] { + auto bytes = Fixture(); + Write(bytes, 32, 0); + Write(bytes, 40, 0); + Write(bytes, 54, 0); + Write(bytes, 56, 0); + Write(bytes, 58, 0); + Write(bytes, 60, 0); + Write(bytes, 62, 0); + const Relinker::ElfReader reader(bytes); + Require(reader.ReadProgramHeaders().empty() && reader.ReadSectionHeaders().empty()); + RejectAddress(std::move(bytes), 0); + }}, + {"translation truncated program header table", [] { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x2c8, bytes.data() + 64, 56); + Write(bytes, 32, 0x2c8); + Write(bytes, 56, 2); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation below segment and at virtual maximum", [] { + RejectAddress(Fixture(), 0x0fff); + auto bytes = Fixture(); + const auto maximum = std::numeric_limits::max(); + Write(bytes, 80, maximum - 31); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(maximum) == 0x25f); + }}, + {"ordinary translations and zero fill", [] { + const Relinker::ElfReader reader(Fixture()); + Require(reader.TranslateVirtualAddress(0x1000) == 0x240); + Require(reader.TranslateVirtualAddress(0x101f) == 0x25f); + RejectAddress(Fixture(), 0x1020); + }}, + }; + std::size_t failures = 0; + for (const auto& [name, test] : cases) { + try { + test(); + std::cout << "PASS: " << name << '\n'; + } catch (const std::exception& error) { + ++failures; + std::cerr << "FAIL: " << name << ": " << error.what() << '\n'; + } + } + return failures == 0 ? 0 : 1; +} From 3d31a8464ac74d984c7511e9fdf424a52305d303 Mon Sep 17 00:00:00 2001 From: Sanjaysrinivas Date: Sat, 10 Oct 2026 18:06:11 +0200 Subject: [PATCH 082/277] fix(agc): reclaim idle buffers on allocation OOM --- .../Graphics/include/BufferPool.hpp | 6 +- .../Graphics/src/BufferPool.cpp | 78 +++++++- .../Graphics/src/Resources.cpp | 4 +- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 180 +++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 5 files changed, 256 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index b2b4d0978..de95acd54 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -61,12 +61,13 @@ public: // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); static VkDeviceSize SlabBlockBytes(std::size_t capacity); SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); - void PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; + VkDeviceSize PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; std::size_t SlabBlocks(); private: @@ -117,7 +118,8 @@ private: static bool DeviceTierEnabled(); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); - void destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize trim(); // Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 2c0c39987..4ae8a5074 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -41,6 +41,30 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation) { + VkDeviceMemory memory = VK_NULL_HANDLE; + auto result = allocateMemory(device, &allocation, nullptr, &memory); + const auto initialResult = result; + unsigned attempts = 1; + VkDeviceSize reclaimed = 0; + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) { + reclaimed = trim(); + if (reclaimed != 0) { + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } + if (result == VK_SUCCESS) { + if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(initialResult), attempts); + return memory; + } + char details[512]; + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(initialResult), attempts); + Check(result, details); + return VK_NULL_HANDLE; +} + bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false; const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity); @@ -80,7 +104,7 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if (addressable) allocation.pNext = &flags; allocation.allocationSize = blockBytes; allocation.memoryTypeIndex = memoryType; - Check(allocateMemory(device, &allocation, nullptr, &block->memory), "vkAllocateMemory buffer slab"); + block->memory = AllocateMemory(allocation, "vkAllocateMemory buffer slab"); if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { void* mapping = nullptr; const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping); @@ -105,26 +129,27 @@ void BufferPool::freeBlock(SlabBlock& block) noexcept { freeMemory(device, block.memory, nullptr); } -void BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { +VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { std::unique_ptr released; { std::lock_guard lock(slabMutex); const auto found = slabBlocks.find(memory); - if (found == slabBlocks.end()) return; + if (found == slabBlocks.end()) return 0; auto& block = *found->second; auto& slab = slabs[block.slab]; if (block.free.empty()) slab.available.push_back(&block); block.free.push_back(static_cast(offset / block.slotBytes)); - if (--block.used != 0) return; + if (--block.used != 0) return 0; if (slab.emptyBlocks == 0) { ++slab.emptyBlocks; - return; + return 0; } slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); released = std::move(found->second); slabBlocks.erase(found); } freeBlock(*released); + return SlabBlockBytes(released->slotBytes); } std::size_t BufferPool::SlabBlocks() { @@ -150,16 +175,16 @@ VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& me return std::max(VkDeviceSize{512} << 20u, largest / 8u); } -void BufferPool::destroy(const BufferAllocation& allocation) noexcept { +VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { if (allocation.slab) { destroyBuffer(device, allocation.buffer, nullptr); - PutSlot(allocation.memory, allocation.offset); - return; + return PutSlot(allocation.memory, allocation.offset); } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); + return allocation.allocationBytes; } bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { @@ -240,6 +265,43 @@ std::size_t BufferPool::MaxSlots() { return slots; } +VkDeviceSize BufferPool::trim() { + Tier retiredSmall; + Tier retiredLarge; + Tier retiredDevice; + { + std::lock_guard lock(mutex); + for (const auto& [tier, retired] : {std::pair{&smallTier, &retiredSmall}, std::pair{&largeTier, &retiredLarge}, std::pair{&deviceTier, &retiredDevice}}) { + tier->free.swap(retired->free); + tier->evictions += tier->slots; + tier->retainedBytes = 0; + tier->slots = 0; + } + } + VkDeviceSize bytes = 0; + for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { + for (const auto& [key, slots] : tier->free) { + for (const auto& slot : slots) bytes += destroy(slot.allocation); + } + } + for (;;) { + std::unique_ptr released; + { + std::lock_guard lock(slabMutex); + const auto found = std::find_if(slabBlocks.begin(), slabBlocks.end(), [](const auto& entry) { return entry.second->used == 0; }); + if (found == slabBlocks.end()) break; + auto& slab = slabs.at(found->second->slab); + slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get())); + --slab.emptyBlocks; + released = std::move(found->second); + slabBlocks.erase(found); + } + freeBlock(*released); + bytes += SlabBlockBytes(released->slotBytes); + } + return bytes; +} + void BufferPool::Put(const BufferAllocation& allocation) noexcept { // Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector // may throw on growth; a Put that cannot retain simply destroys, as noexcept requires. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 70026869a..c21f1d06a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -103,7 +103,7 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag ready = true; return; } - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory"); initializeAddress(usage); if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); @@ -182,7 +182,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa allocation.allocationSize = ClassAllocationBytes(requirements, capacity); allocationBytes = allocation.allocationSize; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index e1379137b..3ab9d30d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,11 @@ struct MockDevice { VkDeviceSize liveBytes = 0; std::map memoryBytes; std::map> bound; + std::set liveBuffers; + std::deque results; + VkDeviceSize limit = 0; + VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; + std::uint64_t attempts = 0; }; MockDevice mock; @@ -45,6 +51,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn *buffer = reinterpret_cast(static_cast(mock.next++)); mock.sizes[*buffer] = info->size; mock.usages[*buffer] = info->usage; + mock.liveBuffers.insert(*buffer); return VK_SUCCESS; } @@ -53,6 +60,17 @@ VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer bu } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + ++mock.attempts; + auto result = VK_SUCCESS; + if (!mock.results.empty()) { + result = mock.results.front(); + mock.results.pop_front(); + } + if (result == VK_SUCCESS && mock.limit != 0 && mock.liveBytes + info->allocationSize > mock.limit) result = mock.pressureResult; + if (result != VK_SUCCESS) { + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return result; + } *memory = reinterpret_cast(static_cast(mock.next++)); if (info->memoryTypeIndex == 1) mock.hostMemory[*memory].resize(info->allocationSize); mock.memoryBytes[*memory] = info->allocationSize; @@ -74,11 +92,16 @@ VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, Vk VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {} -VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); ++mock.destroyedBuffers; } VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + if (!mock.memoryBytes.contains(memory)) { + Expect(false, "an undefined failed allocation output was freed"); + return; + } mock.liveBytes -= mock.memoryBytes.at(memory); mock.memoryBytes.erase(memory); mock.hostMemory.erase(memory); @@ -319,6 +342,149 @@ void Slabs() { Expect(mock.liveBytes == 0, "slab blocks outlived their pool"); } +void AllocationRecovery(VkResult pressureResult, bool host) { + mock = MockDevice{}; + { + auto context = mockContext(); + const auto make = [&](std::size_t bytes) -> std::shared_ptr { + if (host) return std::make_shared(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + return std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + }; + auto active = make(2 * MiB); + const auto activeBuffer = *mock.liveBuffers.begin(); + const auto activeMemory = mock.memoryBytes.begin()->first; + { auto idle = make(2 * MiB); } + mock.limit = 6 * MiB; + mock.pressureResult = pressureResult; + const auto before = mock.attempts; + try { + auto recovered = make(4 * MiB); + Expect(mock.attempts == before + 2, "pool pressure did not trigger exactly one allocation retry"); + Expect(mock.frees == 1 && mock.liveBytes == 6 * MiB, "recovery did not release the idle allocation"); + Expect(mock.liveBuffers.contains(activeBuffer) && mock.memoryBytes.contains(activeMemory), "recovery destroyed an active allocation"); + } catch (const std::exception& error) { + Expect(false, std::string("idle memory did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "recovery leaked a resource"); +} + +void SlabAllocationRecovery() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "a new slab block did not retry after reclaiming an idle allocation"); + Expect(context.bufferPool->SlabBlocks() == 2 && mock.memoryBytes.contains(activeMemory), "slab allocation recovery discarded its live block"); + } catch (const std::exception& error) { + Expect(false, std::string("slab allocation did not recover: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab recovery leaked a resource"); +} + +void ReclaimSlabs() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + std::memset(active.Bytes().data(), 0x5a, active.Bytes().size()); + { + Buffer sameBlock(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer otherBlock(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } + mock.limit = 6 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 4 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "reclaim did not free the unused slab block"); + Expect(context.bufferPool->SlabBlocks() == 1 && mock.memoryBytes.contains(activeMemory), "reclaim freed a slab with a live slot"); + Expect(std::all_of(active.Bytes().begin(), active.Bytes().end(), [](std::byte value) { return value == std::byte{0x5a}; }), "reclaim changed a live slab buffer"); + } catch (const std::exception& error) { + Expect(false, std::string("empty slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab reclaim leaked a resource"); + mock = MockDevice{}; + { + auto context = mockContext(); + BufferPool pool(context); + const auto idle = pool.TakeSlot(context, 1, 512 * 1024, false); + pool.PutSlot(idle.memory, idle.offset); + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = pool.TakeSlot(context, 1, 256 * 1024, false); + Expect(mock.attempts == before + 2 && mock.frees == 1 && pool.SlabBlocks() == 1, "a spare slab without cached buffers was not reclaimed"); + pool.PutSlot(recovered.memory, recovered.offset); + } catch (const std::exception& error) { + Expect(false, std::string("spare slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0, "spare slab recovery leaked device memory"); +} + +void AllocationFailure(VkResult initial, VkResult final, bool cached) { + mock = MockDevice{}; + { + auto context = mockContext(); + if (cached) { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.results = {initial, final}; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + const bool reclaimed = cached && (initial == VK_ERROR_OUT_OF_DEVICE_MEMORY || initial == VK_ERROR_OUT_OF_HOST_MEMORY); + Expect(mock.attempts == before + (reclaimed ? 2 : 1), "allocation retries were not bounded or a non-memory failure was retried"); + Expect(failure.find("Vulkan result " + std::to_string(reclaimed ? final : initial)) != std::string::npos, "allocation failure lost its final Vulkan result"); + Expect(mock.liveBuffers.size() == (cached && !reclaimed ? 1u : 0u), "a failed allocation retained its buffer"); + Expect(mock.liveBytes == (cached && !reclaimed ? 2 * MiB : 0), "a failed allocation mishandled cached memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource"); +} + +void SuccessfulAllocationKeepsPool() { + mock = MockDevice{}; + { + auto context = mockContext(); + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == 2 && mock.frees == 0 && mock.liveBytes == 6 * MiB, "a successful first allocation reclaimed its cache"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "normal allocation leaked a resource"); +} + +void PartialSlabDoesNotRetry() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + { Buffer idle(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + Buffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(failure.find("Vulkan result -2") != std::string::npos, "a live slab's memory was treated as reclaimable"); + Expect(mock.attempts == before + 1 && mock.frees == 0, "an allocation retried without freeing device memory"); + Expect(mock.liveBuffers.size() == 1 && mock.liveBuffers.contains(active.Handle()) && context.bufferPool->SlabBlocks() == 1, "reclaim changed a live slab allocation"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource"); +} + } int main() { @@ -330,6 +496,18 @@ int main() { AddressAndHostUnchanged(); SmallDeviceClasses(); Slabs(); + for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { + AllocationRecovery(result, false); + AllocationRecovery(result, true); + AllocationFailure(result, result, true); + AllocationFailure(result, result, false); + } + SlabAllocationRecovery(); + ReclaimSlabs(); + AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true); + AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); + SuccessfulAllocationKeepsPool(); + PartialSlabDoesNotRetry(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..a13247ac8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -359,6 +359,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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. - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. +- [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy From 7fdc5b452845a2dc7ec2f1dad53a973515a9dd82 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 15:51:43 +0200 Subject: [PATCH 083/277] fix(libkernel): honor null clock_getres output Return success for a null POSIX resolution output before inspecting the clock ID, matching FreeBSD 11.4 sys_clock_getres. AI-assisted: yes --- core/libs/prx/libkernel/Time/Time.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 581e81bb5..0f86dbf4d 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -473,8 +473,7 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { } int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { - KernelTimespec ignoredResolution{}; - if (res == nullptr) res = &ignoredResolution; + if (res == nullptr) return 0; if (IsCpuClock(clockId)) return clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, res); if (clockId == 1 || clockId == 2) { From 10e822cf04763c1b20e39848cb5f975eb7792f2b Mon Sep 17 00:00:00 2001 From: QRMOE Date: Sun, 11 Oct 2026 15:53:15 +0200 Subject: [PATCH 084/277] test(libkernel): cover omitted clock resolution Cover 20 clock IDs, preserved guest errno, non-null unsupported clocks and the SCE null-output fault. The regression fails against main and passes with the accompanying fix. AI-assisted: yes --- core/libs/tests/GuestFreebsdClocks.cpp | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index be5f30434..d75ca2ec4 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -1,13 +1,17 @@ #include "SceTypes.hpp" #include +#include #include +#include #include +#include extern "C" { int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); @@ -32,6 +36,34 @@ static constexpr std::int64_t GIVE_UP_NANOS = 10 * NANOS_PER_SECOND; static void Require(bool value) { if (!value) std::abort(); } +static bool OmittedResolutionSucceeds() { + constexpr int clocks[] = {0, 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 6, 9999, -1, INT_MIN, INT_MAX}; + for (const int clock : clocks) { + *__error_nid_postfix() = 77; + try { + if (clock_getres_nid_postfix(clock, nullptr) == SCE_OK && *__error_nid_postfix() == 77) continue; + } catch (const std::runtime_error&) { + } + std::fprintf(stderr, "clock_getres(%d, nullptr) must succeed without changing errno\n", clock); + return false; + } + return true; +} + +static void UnsupportedResolutionStillThrows() { + KernelTimespec resolution{-1, -1}; + bool rejected = false; + try { + clock_getres_nid_postfix(3, &resolution); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && resolution.tv_sec == -1 && resolution.tv_nsec == -1); + constexpr int fault = static_cast(0x8002000eu); + Require(sceKernelClockGetres(GUEST_CLOCK_REALTIME, nullptr) == fault); + Require(sceKernelClockGetres(3, nullptr) == fault); +} + static KernelTimespec Read(int clockId) { KernelTimespec time{-1, -1}; Require(clock_gettime_nid_postfix(clockId, &time) == SCE_OK); @@ -168,6 +200,8 @@ static void CalendarConversionsInitializeTheirState() { } int main() { + if (!OmittedResolutionSucceeds()) return 1; + UnsupportedResolutionStillThrows(); CalendarConversionsInitializeTheirState(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; From 56d08f254737bfe921bc5122439c240bf63bedb3 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Sun, 11 Oct 2026 10:42:29 -0300 Subject: [PATCH 085/277] fix(agc): exclude host arena holes from write-watch proofs --- .../Execution/src/GuestMemory.cpp | 15 ++++++++ .../libSceAgcDriver/tests/BdaResources.cpp | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 36d7b2817..f82616343 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1058,6 +1058,10 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, } std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return 0; +#endif auto& tracker = Tracker(); // Whole pages, so a page shared with the next range is collected with either. constexpr std::uint64_t page = 4096; @@ -1185,6 +1189,10 @@ bool WriteWatched() { } bool Watched(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1192,6 +1200,10 @@ bool Watched(std::uint64_t address, std::size_t bytes) { } bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1208,6 +1220,9 @@ bool UnchangedSinceAll(std::span queries) { if (!tracker.watched) return false; for (const auto& [address, bytes, generation] : queries) { if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); if (!tracker.unchanged(first, last, generation)) return false; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 032e0b9d7..777c3fa77 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -746,6 +746,42 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { releaseSparse(block, reserved); } +void hostWriteWatchExclusionTests() { +#ifdef _WIN32 + if (AgcDriver::GuestMemory::WriteWatched()) { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + bool found = false; + for (auto cursor = arena; cursor < arena + arenaBytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) { + Require(!AgcDriver::GuestMemory::Watched(cursor, 1), "an arena host hole was reported as write-watched"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(cursor, 1) == 0, "an arena host hole returned a write generation"); + Require(!AgcDriver::GuestMemory::UnchangedSince(cursor, 1, 1), "an arena host hole passed an unchanged proof"); + const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{cursor, 1, 1}}; + Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "an arena host hole passed a bulk unchanged proof"); + found = true; + break; + } + const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; + Require(next > cursor, "arena enumeration did not advance"); + cursor = next; + } + Require(found, "write-watched arena contained no host exclusion to test"); + } + std::vector host(64, 0x31); + const auto address = reinterpret_cast(host.data()); + Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, host.size()) == 0, "ordinary host heap memory returned a guest write generation"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address, host.size(), 1), "ordinary host heap memory was proven unchanged"); + const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{address, host.size(), 1}}; + Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "a host heap range passed a bulk unchanged proof"); + Require(std::all_of(host.begin(), host.end(), [](std::uint8_t value) { return value == 0x31; }), "write-watch eligibility changed host bytes"); +#endif +} + void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { #ifdef _WIN32 if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { @@ -1077,6 +1113,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { gpuMappingTests(context); stridedOverhangTests(context, access); sparseHeapTests(context, access); + hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); From 1ead4a120c07b80e7ca8cfbaa89a53da11c39e54 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:57:20 +0200 Subject: [PATCH 086/277] feat(libc): implement guest fstatvfs volume statistics --- .../prx/libSceLibcInternal/CMakeLists.txt | 17 +++- .../FilesystemStatistics.cpp | 46 +++++++++ .../libkernel/File/include/KernelStatfs.hpp | 33 +++++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 31 +----- core/libs/tests/GuestFstatvfs.cpp | 97 +++++++++++++++++++ core/libs/tests/GuestFstatvfsConversion.cpp | 78 +++++++++++++++ 6 files changed, 271 insertions(+), 31 deletions(-) create mode 100644 core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp create mode 100644 core/libs/prx/libkernel/File/include/KernelStatfs.hpp create mode 100644 core/libs/tests/GuestFstatvfs.cpp create mode 100644 core/libs/tests/GuestFstatvfsConversion.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 40dbd1e76..f3f1f0cdd 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,9 +1,24 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp UserContext.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp UserContext.cpp FilesystemStatistics.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) configure_windows_unwind(libSceLibcInternal) if(BUILD_TESTING) + add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) + target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_fstatvfs_conversion_tests) + add_test(NAME guest_fstatvfs_conversion COMMAND guest_fstatvfs_conversion_tests) + set_tests_properties(guest_fstatvfs_conversion PROPERTIES TIMEOUT 20) + add_test_executable(guest_fstatvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfs.cpp) + target_include_directories(guest_fstatvfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstatvfs_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_fstatvfs_tests) + add_test(NAME guest_fstatvfs COMMAND guest_fstatvfs_tests) + set_tests_properties(guest_fstatvfs PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp) target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_utime_forwarding_tests) diff --git a/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp new file mode 100644 index 000000000..083431cef --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp @@ -0,0 +1,46 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include +#include + +struct LibcStatvfs { + std::uint64_t f_bavail; + std::uint64_t f_bfree; + std::uint64_t f_blocks; + std::uint64_t f_favail; + std::uint64_t f_ffree; + std::uint64_t f_files; + std::uint64_t f_bsize; + std::uint64_t f_flag; + std::uint64_t f_frsize; + std::uint64_t f_fsid; + std::uint64_t f_namemax; +}; +static_assert(sizeof(LibcStatvfs) == 88); +static_assert(offsetof(LibcStatvfs, f_bsize) == 48); +static_assert(offsetof(LibcStatvfs, f_namemax) == 80); + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* output) { + int* const error = __error_nid_postfix(); + const int saved = *error; + KernelStatfs volume{}; + const int status = _fstatfs_nid_postfix(descriptor, &volume); + if (status != 0) return status; + if (output == nullptr) { + *error = 14; + return -1; + } + const LibcStatvfs result{ + static_cast(volume.f_bavail), volume.f_bfree, volume.f_blocks, + static_cast(volume.f_ffree), static_cast(volume.f_ffree), volume.f_files, + volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize, + 0, volume.f_namemax + }; + std::memcpy(output, &result, sizeof(result)); + *error = saved; + return 0; +} diff --git a/core/libs/prx/libkernel/File/include/KernelStatfs.hpp b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp new file mode 100644 index 000000000..7ecfe985f --- /dev/null +++ b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +struct KernelStatfs { + std::uint32_t f_version; + std::uint32_t f_type; + std::uint64_t f_flags; + std::uint64_t f_bsize; + std::uint64_t f_iosize; + std::uint64_t f_blocks; + std::uint64_t f_bfree; + std::int64_t f_bavail; + std::uint64_t f_files; + std::int64_t f_ffree; + std::uint64_t f_syncwrites; + std::uint64_t f_asyncwrites; + std::uint64_t f_syncreads; + std::uint64_t f_asyncreads; + std::uint64_t f_spare[10]; + std::uint32_t f_namemax; + std::uint32_t f_owner; + std::int32_t f_fsid[2]; + char f_charspare[80]; + char f_fstypename[16]; + char f_mntfromname[88]; + char f_mntonname[88]; +}; +static_assert(sizeof(KernelStatfs) == 472); +static_assert(offsetof(KernelStatfs, f_bavail) == 48); +static_assert(offsetof(KernelStatfs, f_namemax) == 184); +static_assert(offsetof(KernelStatfs, f_fstypename) == 280); +static_assert(offsetof(KernelStatfs, f_mntonname) == 384); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..5b728b8da 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -8,6 +8,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -31,36 +32,6 @@ static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec)); static constexpr int KERNEL_IOV_MAX = 1024; -struct KernelStatfs { - std::uint32_t f_version; - std::uint32_t f_type; - std::uint64_t f_flags; - std::uint64_t f_bsize; - std::uint64_t f_iosize; - std::uint64_t f_blocks; - std::uint64_t f_bfree; - std::int64_t f_bavail; - std::uint64_t f_files; - std::int64_t f_ffree; - std::uint64_t f_syncwrites; - std::uint64_t f_asyncwrites; - std::uint64_t f_syncreads; - std::uint64_t f_asyncreads; - std::uint64_t f_spare[10]; - std::uint32_t f_namemax; - std::uint32_t f_owner; - std::int32_t f_fsid[2]; - char f_charspare[80]; - char f_fstypename[16]; - char f_mntfromname[88]; - char f_mntonname[88]; -}; -static_assert(sizeof(KernelStatfs) == 472); -static_assert(offsetof(KernelStatfs, f_bavail) == 48); -static_assert(offsetof(KernelStatfs, f_namemax) == 184); -static_assert(offsetof(KernelStatfs, f_fstypename) == 280); -static_assert(offsetof(KernelStatfs, f_mntonname) == 384); - static constexpr std::uint32_t KERNEL_STATFS_VERSION = 0x20030518; static constexpr std::uint64_t KERNEL_MNT_RDONLY = 0x1; static constexpr std::uint64_t KERNEL_MNT_NOEXEC = 0x4; diff --git a/core/libs/tests/GuestFstatvfs.cpp b/core/libs/tests/GuestFstatvfs.cpp new file mode 100644 index 000000000..487e8bacc --- /dev/null +++ b/core/libs/tests/GuestFstatvfs.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fstatvfs_nid_postfix(int, void*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireFailure(int descriptor, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + Require(fstatvfs_nid_postfix(descriptor, output.data()) == -1); + Require(*__error_nid_postfix() == error); + Require(output == saved); +} + +static void RequireVolume(int descriptor, const std::filesystem::path& path) { + std::array guarded; + guarded.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(fstatvfs_nid_postfix(descriptor, guarded.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front()); + const auto* fields = guarded.data() + 1; + const auto capacity = std::filesystem::space(path).capacity; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] > 0 && fields[2] == capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require(fields[3] == fields[4]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto filePath = root / "file.txt"; + { + std::ofstream file(filePath, std::ios::binary); + file << "fstatvfs"; + Require(static_cast(file)); + } + AddPathAlias_nid_no_patch("/anyps5-fstatvfs-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstatvfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + Require(sceKernelLseek(file, 3, 0) == 3); + RequireVolume(file, filePath); + Require(sceKernelLseek(file, 0, 1) == 3); + Require(fstatvfs_nid_postfix(file, nullptr) == -1 && *__error_nid_postfix() == 14); + + const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + RequireVolume(directory, root); + Require(close_nid_postfix(directory) == 0); + + int pipes[2]; + Require(pipe_nid_postfix(pipes) == 0); + RequireFailure(pipes[0], 22); + RequireFailure(pipes[1], 22); + for (int fd : pipes) Require(close_nid_postfix(fd) == 0); + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], 22); + for (int fd : sockets) Require(close_nid_postfix(fd) == 0); + RequireFailure(sockets[0], 9); + RequireFailure(-1, 9); + Require(close_nid_postfix(file) == 0); + RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test"); + Require(std::filesystem::remove(filePath)); + Require(std::filesystem::remove(root)); +} diff --git a/core/libs/tests/GuestFstatvfsConversion.cpp b/core/libs/tests/GuestFstatvfsConversion.cpp new file mode 100644 index 000000000..f93e221e6 --- /dev/null +++ b/core/libs/tests/GuestFstatvfsConversion.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int, void*); + +namespace { +KernelStatfs backend; +int backendError; +int guestError; +int calls; + +void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs conversion check failed at line %d\n", line); + std::abort(); + } +} +} +#define Require(value) Check((value), __LINE__) + +extern "C" int* APS5_VABI __error_nid_postfix() { return &guestError; } + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int descriptor, KernelStatfs* output) { + Require(descriptor == 42); + ++calls; + *output = backend; + guestError = backendError ? backendError : 71; + return backendError ? -1 : 0; +} + +int main() { + backend.f_blocks = 0x100000002ull; + backend.f_bfree = 0x100000001ull; + backend.f_bavail = 0x100000000ll; + backend.f_files = 0x200000002ull; + backend.f_ffree = 0x200000001ll; + backend.f_iosize = 65536; + backend.f_bsize = 4096; + backend.f_namemax = 255; + backend.f_fsid[0] = 123; + for (std::uint64_t flags = 0; flags != 16; ++flags) { + backend.f_flags = flags; + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + const std::array expected{ + 0x100000000ull, 0x100000001ull, 0x100000002ull, + 0x200000001ull, 0x200000001ull, 0x200000002ull, + 65536, (flags & 1) | ((flags & 8) ? 2u : 0u), 4096, 0, 255 + }; + const int before = calls; + guestError = 12345; + Require(fstatvfs_nid_postfix(42, output.data() + 1) == 0); + Require(calls == before + 1 && guestError == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + Require(std::memcmp(output.data() + 1, expected.data(), sizeof(expected)) == 0); + } + backend.f_bavail = -1; + backend.f_ffree = -2; + std::array output{}; + Require(fstatvfs_nid_postfix(42, output.data()) == 0); + Require(output[0] == std::numeric_limits::max()); + Require(output[3] == std::numeric_limits::max() - 1 && output[4] == output[3]); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == 14); + for (int failure : {5, 9, 22, 45}) { + backendError = failure; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + Require(fstatvfs_nid_postfix(42, output.data()) == -1 && guestError == failure); + Require(output == saved); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == failure); + } +} From b4b2e8843af2be7a5a1228fafe7ab21bce97b887 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:59:53 +0200 Subject: [PATCH 087/277] docs(TechnicalDebt): list sceRazorCpuPushMarker under silent stubs Co-Authored-By: Claude Opus 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b84b39c4c..31a510b9d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -44,6 +44,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device +- [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 ### Unknown function info @@ -248,7 +249,6 @@ Throughout the project, every function at every stage either **does exactly what - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info - [sceRazorCpuFlushOccurred](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture -- [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [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 From f32bbf2e9e230956e9e1b2ee3f3e777f7a4c5c35 Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:02:45 +0200 Subject: [PATCH 088/277] fix(cache): replace entries while Windows readers retain old files Publish Windows cache entries with the POSIX rename operation so an open delete-sharing reader keeps its original file after replacement. Close the rename handle before returning and retain the existing fallback only when the extended operation is unsupported. Exercise old and new reader contents, deletion lifetime and temporary file cleanup in the registered disk-cache regression on both platforms. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 67 +++++++++++++++++++ .../recompiler/ShaderCacheDirectory.hpp | 41 ++++++++++++ 2 files changed, 108 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index fee50cb71..b0822a499 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -23,6 +23,15 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif namespace { @@ -487,6 +496,63 @@ void verifyFloatModeSerialization() { require(back.request.context.floatMode == mode, "the float mode did not survive serialization"); } +void verifyAtomicReplacement(const std::filesystem::path& directory) { + struct Reader { +#ifdef _WIN32 + HANDLE file; + explicit Reader(const std::filesystem::path& path) + : file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) { + require(file != INVALID_HANDLE_VALUE, "cannot retain the cache file"); + } + ~Reader() { CloseHandle(file); } + std::vector Read() { + require(SetFilePointerEx(file, {}, nullptr, FILE_BEGIN), "cannot seek the retained cache file"); + std::array bytes{}; + DWORD count = 0; + require(ReadFile(file, bytes.data(), static_cast(bytes.size()), &count, nullptr), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + count}; + } +#else + std::ifstream file; + explicit Reader(const std::filesystem::path& path) : file(path, std::ios::binary) { + require(file.is_open(), "cannot retain the cache file"); + } + std::vector Read() { + file.clear(); + file.seekg(0); + require(!file.fail(), "cannot seek the retained cache file"); + std::array bytes{}; + file.read(reinterpret_cast(bytes.data()), bytes.size()); + require(file.eof() && !file.bad(), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + file.gcount()}; + } +#endif + Reader(const Reader&) = delete; + Reader& operator=(const Reader&) = delete; + }; + const auto root = directory / "atomic-replacement"; + const auto path = root / "entry.bin"; + 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"); + { + Reader old(path); + require(old.Read() == original, "the retained reader did not read the original entry"); + require(WriteFileAtomically(path, replacement), "cannot replace cache entry with a retained reader"); + require(old.Read() == original, "replacement changed the retained original entry"); + std::vector fresh; + require(ReadWholeFile(path, fresh) && fresh == replacement, "a fresh reader did not read the replacement entry"); + Reader current(path); + require(current.Read() == replacement, "the replacement reader did not retain its entry"); + require(std::filesystem::remove(path), "cannot delete the replacement while readers retain it"); + require(!ReadWholeFile(path, fresh), "the deleted cache entry remains readable by path"); + require(old.Read() == original && current.Read() == replacement, "deletion invalidated retained cache readers"); + } + require(std::filesystem::is_empty(root), "atomic replacement left a temporary file behind"); + require(std::filesystem::remove(root), "cannot remove the empty replacement directory"); +} + void verifyStore() { require(ShaderDiskCache::Enabled(), "the disk cache is not enabled"); SampleRequest sample; @@ -1034,6 +1100,7 @@ int main(int argc, char** argv) { verifyDefaultDirectory(argv[0]); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); verifyArtifactStorageIsolation(); diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index 229506482..e17f9b110 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -14,6 +14,15 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif namespace ShaderRecompiler { @@ -26,6 +35,19 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span{}(std::this_thread::get_id()) ^ static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); auto temporary = path; temporary += ".tmp." + std::to_string(unique) + "." + std::to_string(serial.fetch_add(1, std::memory_order_relaxed)); +#ifdef _WIN32 + struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; + static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); + const auto destination = std::filesystem::absolute(path, error); + if (error || destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; + const auto nameBytes = destination.native().size() * sizeof(wchar_t); + std::vector renameBuffer(sizeof(RenameInfo) + nameBytes); + auto* rename = reinterpret_cast(renameBuffer.data()); + constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002; + rename->flags = replaceExisting | posixSemantics; + rename->nameBytes = static_cast(nameBytes); + std::memcpy(rename->name, destination.c_str(), nameBytes); +#endif bool written = false; { std::ofstream file(temporary, std::ios::binary | std::ios::trunc); @@ -38,6 +60,25 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span(22); + if (!SetFileInformationByHandle(handle, renameInfoEx, rename, static_cast(renameBuffer.size()))) renameError = GetLastError(); + } + if (renameError == ERROR_SUCCESS) return true; + if (renameError != ERROR_INVALID_PARAMETER && renameError != ERROR_INVALID_FUNCTION && renameError != ERROR_NOT_SUPPORTED) { + std::filesystem::remove(temporary, error); + return false; + } +#endif std::filesystem::rename(temporary, path, error); if (error) { std::filesystem::remove(temporary, error); From 76fa2becebef585658bf1a910ea50d78056ce33f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:06:57 +0200 Subject: [PATCH 089/277] perf(recompiler): reuse sorted sets for iteration selection merges Seed each iteration-selection update from its first eligible successor instead of rebuilding and intersecting the universe of block IDs. Reuse the existing in-place intersection and sorted-insertion helpers while preserving the original empty-successor and fixed-point behavior. Keep a cache-neutral switch for same-binary output and timing comparisons. Run the existing structurizer and control-flow fixtures in a separate process with this optimization disabled as well as in their default mode. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++++- .../ControlFlow/src/Structurizer.cpp | 25 +++++++++++++++---- core/shader/recompiler/ShaderDiskCache.cpp | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..6956e6e5a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1021,8 +1021,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests) set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests) + 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}") if(WIN32) - set_tests_properties(agc_structurizer_intersection_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 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/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index e4cb6e53b..70193cd42 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -428,6 +428,7 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const NaturalLoop& loop, const BasicBlock& header, std::uint32_t join) { const auto count = static_cast(graph.blocks.size()); + static const bool reuse = std::getenv("APS5_NO_PERF_ITERATION_SELECTION") == nullptr; const auto escapes = [&](std::uint32_t blockId) { return blockId == loop.continueBlock || blockId == loop.mergeBlock; }; std::vector joins(count, false); std::vector pending = {join}; @@ -455,16 +456,30 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur if (!joins[block.id] || block.id == join) { continue; } - auto next = allBlockIds(count); + bool seeded = !reuse; + auto next = seeded ? allBlockIds(count) : std::vector{}; for (const auto successor : block.successors) { if (joins[successor]) { - next = intersectSorted(next, postDominators[successor]); + if (!seeded) { + next = postDominators[successor]; + seeded = true; + } else if (reuse) { + intersectAnalysisSet(next, postDominators[successor]); + } else { + next = intersectSorted(next, postDominators[successor]); + } } } - addUnique(next, block.id); - sortUnique(next); + if (!seeded) next = allBlockIds(count); + if (reuse) { + addSortedUnique(next, block.id); + } else { + addUnique(next, block.id); + sortUnique(next); + } if (next != postDominators[block.id]) { - postDominators[block.id] = std::move(next); + if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); + else postDominators[block.id] = std::move(next); changed = true; } } diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bd05440bc..203770688 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -671,6 +671,7 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_FAILURE_MEMO", "APS5_NO_PERF_FRONTEND_PAIR", "APS5_NO_PERF_DOM_INTERSECTION", + "APS5_NO_PERF_ITERATION_SELECTION", "APS5_NO_RESULT_MEMO", }; From 2f140cb8b0563e243022d471d0b23de9baf0f95d Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sun, 11 Oct 2026 04:09:46 +0200 Subject: [PATCH 090/277] fix(agc): read zero for pixel inputs of parameters the vertex stage does not export A pixel input whose SPI_PS_INPUT_CNTL offset names a parameter the previous stage does not export failed validation ("graphics interfaces disagree"), so the draw threw. No export writes that parameter slot for the draw. radeonsi programs a pixel input without a matching vertex output as OFFSET 0x20 with DEFAULT_VAL 0, so that it reads (0, 0, 0, 0) (SI_PS_INPUT_CNTL_UNUSED in si_shader.h, applied in si_shader.c), and rect-list draws already pass zero for such inputs. Before the pixel stage of a draw is prepared, inputs that read a parameter the previous stage's program does not export now read zero: OFFSET bit 5 is set, DEFAULT_VAL and FLAT_SHADE are cleared, and an FP16 input also reads zero in its high half (USE_DEFAULT_ATTR1 set, DEFAULT_VAL_ATTR1 cleared). Inputs that already read a default value are unchanged. Fixes #2975. --- .../Execution/src/Driver/Draw/Draw.cpp | 9 +++++++- .../Graphics/include/ShaderInputState.hpp | 1 + .../Graphics/src/ShaderInputState.cpp | 10 ++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) 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 fe033b378..6478ed166 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -92,7 +92,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile); const auto& graphics = decode->state; - const auto& pixel = decode->pixel; + auto pixel = decode->pixel; std::vector programs = decode->programs; const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; @@ -204,6 +204,13 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe 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; + pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); + 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)); if (!rejected.empty()) return DrawVerdict::Rejected; programResults[i] = &results.back(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 671937306..a198d17b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::Graphics { ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram = false); +ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 486df0e35..60c0ba0cb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -93,6 +93,16 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } +ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports) { + for (std::uint32_t input = 0; input < pixel.interpolatorCount && input < pixel.interpolatorSettings.size(); ++input) { + auto& setting = pixel.interpolatorSettings[input]; + if ((setting & 0x20u) != 0u || std::ranges::find(parameterExports, setting & 0x1fu) != parameterExports.end()) continue; + setting = (setting & ~0x700u) | 0x20u; + if ((setting & 0x80000u) != 0u) setting = (setting & ~0x600000u) | 0x100000u; + } + return pixel; +} + ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram) { const auto inControl = nullProgram ? 0u : read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..82b8c28a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -731,6 +731,28 @@ void HeaderlessVertexStageTests() { expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(truncated, 0x1000, userData)); }, "smaller than the fixed AGC header"); } +void UnexportedPixelInputTests() { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 5; + pixel.interpolatorSettings[0] = 0x000u; + pixel.interpolatorSettings[1] = 0x401u; + pixel.interpolatorSettings[2] = 0x302u; + pixel.interpolatorSettings[3] = 0x123u; + pixel.interpolatorSettings[4] = 0x680004u; + pixel.interpolatorSettings[5] = 0x005u; + const std::array exported{0u}; + const auto adjusted = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, exported); + Require(adjusted.interpolatorSettings[0] == 0x000u, "an input of an exported parameter was changed"); + Require(adjusted.interpolatorSettings[1] == 0x021u, "a flat input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[2] == 0x022u, "an input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[3] == 0x123u, "an input that already reads its default value was changed"); + Require(adjusted.interpolatorSettings[4] == 0x180024u, "an FP16 input of an unexported parameter did not read zero in both halves"); + Require(adjusted.interpolatorSettings[5] == 0x005u, "a setting past SPI_PS_IN_CONTROL's input count was changed"); + const std::array all{0u, 1u, 2u, 4u, 5u}; + const auto unchanged = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, all); + Require(std::ranges::equal(unchanged.interpolatorSettings, pixel.interpolatorSettings), "inputs of exported parameters were changed"); +} + void PixelInputLayoutTests() { using ShaderRecompiler::PixelInput; using ShaderRecompiler::PixelInputVgpr; @@ -4314,6 +4336,7 @@ int main(int argc, char** argv) { NggVertexWorkgroupTests(); TuningFieldTests(); PixelInputLayoutTests(); + UnexportedPixelInputTests(); ColorPipeBankXorTests(); HeaderlessVertexStageTests(); ComputeScratchTests(); From baf376862f37ff15f47a76f10f5cd0d32f8eab03 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:19:53 +0100 Subject: [PATCH 091/277] perf(shader): avoid copying SRT read uses --- .../recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index e3e0af422..4da4c6009 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -103,9 +103,7 @@ void PlanBuilder::PatchReads() { IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); block->RemoveInstruction(&flat); block->InsertInstructionBefore(patch.inst, &flat); - for (const auto& use : std::vector(patch.inst->OperandUses())) { - use.user->ReplaceArgument(use.operand, &flat); - } + patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { if (info.condition != nullptr && info.condition->Resolve() == patch.inst) { info.condition = ♭ From d6bc5027659d6661bf5c71ba9c52cdf8ffc5eb1c Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:20:18 +0400 Subject: [PATCH 092/277] fix(import-audit): validate ELF and PE exports before reporting --- tools/import_audit.py | 96 +++++++++++++--- tools/tests/test_import_audit.py | 184 ++++++++++++++++++++++++++++++- 2 files changed, 256 insertions(+), 24 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index c7ebf9dc8..a6c0f6b0d 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,4 +1,5 @@ import argparse +import hashlib import json import re import struct @@ -33,14 +34,14 @@ def unpack(layout, data, offset, path, what): return struct.unpack(layout, take(data, offset, struct.calcsize(layout), path, what)) -def cstring(data, offset, limit, path, what): +def cstring(data, offset, limit, path, what, encoding="utf-8", errors="surrogateescape"): end = data.find(b"\0", offset, limit) if offset < 0 or offset >= limit or end < 0: raise AuditError(f"{path}: {what} at 0x{offset:x} is not a NUL-terminated string inside 0x{limit:x} bytes") try: - return data[offset:end].decode("utf-8") + return data[offset:end].decode(encoding, errors=errors) except UnicodeDecodeError as error: - raise AuditError(f"{path}: {what} at 0x{offset:x} is not UTF-8: {error}") from error + raise AuditError(f"{path}: {what} at 0x{offset:x} is not {encoding}: {error}") from error def elf_exports(path, data): @@ -48,9 +49,14 @@ def elf_exports(path, data): ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") + if not section_offset: + raise AuditError(f"{path}: ELF exports without a section header table are not supported") + if not section_count: + section_count = unpack(" 1: + raise AuditError(f"{path}: multiple extended section index tables for section {section_index}") for position in range(0, size, 24): name, info, other, index, _, _ = unpack("= len(sections): + raise AuditError(f"{path}: extended symbol section index {index} lies outside the section table") + elif 0 < index < 0xff00 and index >= len(sections): + raise AuditError(f"{path}: symbol section index {index} lies outside the section table") binding, visibility = info >> 4, other & 3 - if index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): + if name == 0 or index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): continue names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name")) return names @@ -79,12 +96,19 @@ def pe_exports(path, data): directory = PE_DIRECTORY_OFFSET[magic] if optional_size < directory + 8: raise AuditError(f"{path}: PE optional header of {optional_size} bytes has no export directory") + take(data, optional, optional_size, path, "PE optional header") + if unpack("= raw_size or length > raw_size - relative: continue take(data, raw_offset + relative, length, path, f"data at RVA 0x{address:x}") - return raw_offset + relative + return raw_offset + relative, min(raw_offset + raw_size, len(data)) raise AuditError(f"{path}: RVA 0x{address:x} is not inside any section") - table = unpack("= function_count: + raise AuditError(f"{path}: export ordinal {ordinal} lies outside the address table ({function_count} entries)") + function_rva = unpack(" Date: Sun, 11 Oct 2026 18:21:09 +0400 Subject: [PATCH 093/277] fix(conventions): handle unusual Git paths and escaped links --- tools/check_conventions.py | 35 +++--- .../tests/test_check_conventions_comments.py | 101 ++++++++++++++++++ 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8254975e8..961b82c97 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -7,6 +7,7 @@ import subprocess import sys from collections import defaultdict from pathlib import PurePosixPath +from urllib.parse import unquote RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md in the same pull request; #endif and namespace ends are always allowed"), @@ -55,21 +56,26 @@ class Check: self.added = defaultdict(list) self.status = {} self.numstat = {} - self.tree = set(git("ls-tree", "-r", "--name-only", head).splitlines()) + entries = [entry.split("\t", 1) for entry in git("ls-tree", "-rz", head).split("\0") if entry] + self.modes = {path: metadata.split()[0] for metadata, path in entries} + self.tree = set(self.modes) self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} - for line in git("diff", "--name-status", "--no-renames", self.base, head).splitlines(): - status, path = line.split("\t", 1) + statuses = git("diff", "--name-status", "--no-renames", "-z", self.base, head).split("\0") + for status, path in zip(statuses[::2], statuses[1::2]): self.status[path] = status - for line in git("diff", "--numstat", "--no-renames", self.base, head).splitlines(): + for line in git("diff", "--numstat", "--no-renames", "-z", self.base, head).split("\0"): + if not line: + continue added, deleted, path = line.split("\t", 2) self.numstat[path] = (added, deleted) - path = None - for line in git("diff", "-U0", "--no-color", "--no-renames", "--no-prefix", self.base, head).splitlines(): - if line.startswith("+++ "): - path = None if line == "+++ /dev/null" else line[4:].rstrip("\t") + paths = iter(self.status) + path, number = None, None + for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).splitlines(): + if line.startswith("diff --git "): + path, number = next(paths), None elif line.startswith("@@"): number = int(re.search(r"\+(\d+)", line).group(1)) - elif line.startswith("+") and path: + elif path and number is not None and line.startswith("+"): self.added[path].append((number, line[1:])) number += 1 @@ -141,6 +147,9 @@ class Check: for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue + if self.modes.get(path) == "160000": + self.report("system-dependency", path, 0, "submodule outside 3rdparty/") + continue suffix = PurePosixPath(path).suffix.lower() if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(git("show", f"{self.head}:{path}", text=False))): self.report("binary", path, 0) @@ -188,10 +197,11 @@ def utf8(data): def links(path, text): result = [] for target in LINK.findall(text): - if re.match(r"[a-z]+:|#", target): + if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue target = target.split("#", 1)[0].split("?", 1)[0] - result.append((target, posixpath.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)))) + decoded = unquote(target) + result.append((target, posixpath.normpath(decoded.lstrip("/") if decoded.startswith("/") else str(PurePosixPath(path).parent / decoded)))) return result @@ -256,7 +266,8 @@ def output(check, repo): errors += level == "error" text = f"{message}" + (f": {detail}" if detail else "") if actions: - location = f" file={path},line={max(line, 1)}," if path else " " + escaped_path = path.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A").replace(":", "%3A").replace(",", "%2C") + location = f" file={escaped_path},line={max(line, 1)}," if path else " " print(f"::{level}{location}title={rule}::" + text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")) else: print(f"{path or '(pull request)'}{f':{line}' if line else ''}: {level}: [{rule}] {text}") diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 9c943fe7b..a584fcd23 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -1,3 +1,5 @@ +import contextlib +import io import os import subprocess import sys @@ -7,6 +9,7 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body +from check_conventions import Check, STUB_BODY, function_body, links, output class FunctionBodyTests(unittest.TestCase): @@ -270,5 +273,103 @@ class SystemDependencyTests(unittest.TestCase): self.assertIsNone(SYSTEM_DEPENDENCY.search(line)) +class RepositoryChecksTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + self.original = Path.cwd() + os.chdir(self.root) + self.git("init", "-q") + (self.root / "README.md").write_text("Project\n", encoding="utf-8") + self.commit() + + def tearDown(self): + os.chdir(self.original) + self.directory.cleanup() + + def git(self, *args): + return subprocess.run(["git", "-c", "user.name=Conventions Test", "-c", + "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + capture_output=True, text=True, check=True).stdout.strip() + + def commit(self): + self.git("add", ".") + self.git("commit", "-qm", "test: add convention fixture") + + def test_quoted_utf8_paths_are_checked(self): + name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" + path = self.root / name + path.parent.mkdir(parents=True) + path.write_text("[missing](absent.md)\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.repository() + check.docs() + self.assertEqual(check.findings, [("doc-link", name, 1, "absent.md")]) + + def test_mixed_patch_types_preserve_file_order(self): + name = "a b.cpp" if os.name == "nt" else "a\nb.cpp" + (self.root / "README.md").unlink() + (self.root / "a.bin").write_bytes(b"binary\x00data") + (self.root / name).write_text("// debt\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + check.repository() + self.assertIn(("comment", name, 1, ""), check.findings) + self.assertIn(("binary", "a.bin", 0, ""), check.findings) + self.assertNotIn("README.md", check.added) + + def test_increment_line_is_not_a_patch_file_header(self): + path = self.root / "core/example.cpp" + path.parent.mkdir() + path.write_text("void example() {\n++ counter;\n// debt\n}\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + + def test_submodule_outside_thirdparty_reports_without_crashing(self): + head = self.git("rev-parse", "HEAD") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "vendor") + self.git("commit", "-qm", "build: add invalid submodule") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) + + def test_thirdparty_submodule_remains_allowed(self): + head = self.git("rev-parse", "HEAD") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") + self.git("commit", "-qm", "build: add allowed submodule") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, []) + + def test_relative_link_url_decoding(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), + [("./space%20name.md", "docs/dev/space name.md")]) + self.assertEqual(links("docs/dev/index.md", "[reference](HTTPS://example.com/x)"), []) + self.assertEqual(links("docs/dev/index.md", "[reference](//example.com/x)"), []) + + def test_percent_encoded_fragment_is_part_of_filename(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./part%23one.md#details)"), + [("./part%23one.md", "docs/dev/part#one.md")]) + + def test_annotation_filename_properties_are_escaped(self): + check = type("Findings", (), {"findings": [("doc-link", "docs/name,with:colon%\n.md", 2, "missing") ]})() + original = os.environ.get("GITHUB_ACTIONS") + os.environ["GITHUB_ACTIONS"] = "true" + try: + with contextlib.redirect_stdout(io.StringIO()) as captured: + output(check, "owner/repo") + finally: + if original is None: + os.environ.pop("GITHUB_ACTIONS", None) + else: + os.environ["GITHUB_ACTIONS"] = original + annotation = captured.getvalue().splitlines()[0] + self.assertIn("file=docs/name%2Cwith%3Acolon%25%0A.md,line=2", annotation) + + if __name__ == "__main__": unittest.main() From 14bbb1e00f5494b44e9cddc5be7e6ac082a5d701 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:21:44 +0400 Subject: [PATCH 094/277] fix(pr-overlap): bound explicit dependency declarations --- tools/pr_overlap.py | 2 +- tools/tests/test_pr_overlap_dependencies.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index e84ee058d..279006dba 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -9,7 +9,7 @@ from urllib.parse import urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") -DEPENDS = re.compile(r'Depends on:([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]*#\d+[^\r\n]*)*)') +DEPENDS = re.compile(r'Depends on:[ \t]*((?:#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*|none\b)?(?:\r?\n[ \t]*[-*][ \t]*#\d+\b[^\r\n]*)*)') PLAIN_DEPENDS = re.compile(r'Depends on[ \t]+(#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*)') HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))?", re.M) MARKER = "" diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index 1ce38ff84..f93bb828c 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -54,6 +54,15 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], {2}) + def test_colon_declaration_stops_before_unrelated_prose(self): + for body in ("Depends on: #2. Related #3 covers something else.", + "Depends on: #2; related #3 covers something else.", + "Depends on: none; see issue #3.", + "Depends on: #2, related work is in #3."): + with self.subTest(body=body): + expected = set() if "none" in body else {2} + self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_checklist_declaration_keeps_precedence(self): for declaration, expected in (("#3", {3}), ("none", set())): body = "Previously: Depends on #2.\n\n- [x] Depends on: " + declaration From 08b1b7808aae9fdfc961df0cb3071de073a1fad5 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:22:21 +0400 Subject: [PATCH 095/277] fix(nid-names): ignore comments and literals in symbol inventories --- tools/nid_names.py | 88 ++++++++++++++++++++---------- tools/tests/test_nid_names.py | 100 ++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 28 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 35ee96582..edb87dc69 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -18,7 +18,12 @@ DEFAULT_CACHE = Path(tempfile.gettempdir()) / "anyps5-nid-db" / "aerolib.csv" CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) -ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') +CPP_TOKEN = re.compile( + r'(?P//[^\n]*|/\*[\s\S]*?\*/)' + r'|(?P(?:u8|u|U|L)?R"(?P[^\s()\\]{0,16})\([\s\S]*?\)(?P=delimiter)")' + r'|(?P(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\])*")' + r"|(?P(?:u8|u|U|L)?'(?:\\[\s\S]|[^'\\])*')" + r"|(?P(?:[0-9]|\.[0-9])[A-Za-z0-9_.']*|[A-Za-z_][A-Za-z0-9_]*|\S)") DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" @@ -49,35 +54,58 @@ def load_db(path): finally: temporary.unlink(missing_ok=True) db = {} - with open(path, errors="replace") as handle: + with open(path, encoding="utf-8-sig", errors="replace") as handle: for line in handle: parts = line.strip().split() - if len(parts) >= 2: - db.setdefault(parts[0], parts[1]) + if len(parts) >= 2 and not parts[0].startswith("#"): + nid, name = (part.split("#", 1)[0] for part in parts[:2]) + if nid and name: + db.setdefault(nid, name) return db +def source_files(root): + return (path for path in sorted(root.rglob("*.cpp")) + if not any(part.lower() in {"test", "tests"} + for part in path.relative_to(root).parts[:-1])) + + +def source_aliases(text): + tokens = [match.group() for match in CPP_TOKEN.finditer(text.replace("\\\n", "").replace("\\\r\n", "")) + if match.group("comment") is None] + for index, token in enumerate(tokens): + if token != "APS5_EXPORT" or index + 5 >= len(tokens): + continue + opening, literal, comma, name, closing = tokens[index + 1:index + 6] + if opening != "(" or comma != "," or closing != ")": + continue + if re.fullmatch(r'"[A-Za-z0-9+\-]{11}"', literal) is None: + continue + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) and "unknown" in name.lower(): + yield literal[1:-1], name + + +def source_definitions(text): + def mask(match): + return match.group() if match.group("code") is not None else re.sub(r"[^\n]", " ", match.group()) + + return DEFINITION.finditer(CPP_TOKEN.sub(mask, text.replace("\\\n", "").replace("\\\r\n", ""))) + + def collect_unknowns(): found = {} - for path in sorted(PRX.rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in ALIAS.finditer(text): - found.setdefault(match.group(1), "%s:%s" % ( - path.relative_to(ROOT), match.group(2))) + for path in source_files(PRX): + text = path.read_text(encoding="utf-8") + for nid, name in source_aliases(text): + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT), name)) return found def collect_real_names(): names = set() - for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in DEFINITION.finditer(text): + for path in source_files(ROOT / "core" / "libs"): + text = path.read_text(encoding="utf-8") + for match in source_definitions(text): name = match.group(1) names.add(name) if name.endswith(NID_POSTFIX): @@ -118,26 +146,30 @@ def nid_arguments(arguments, options): return normalized -def main(): +def main(argv=None): parser = argparse.ArgumentParser( description="Suggest real names for Unknown NID stubs") parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), help="NID database CSV (downloaded to a cache dir by default)") - parser.add_argument("--nid", action="append", default=[], + parser.add_argument("--nid", action="append", default=None, help="resolve only these NIDs instead of scanning the tree") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") options = [option for action in parser._actions for option in action.option_strings] - args = parser.parse_args(nid_arguments(sys.argv[1:], options)) + arguments = sys.argv[1:] if argv is None else argv + args = parser.parse_args(nid_arguments(arguments, options)) + requested_nids = any(option_name(argument, options) == "--nid" for argument in arguments) db_path = Path(args.db) if args.db else DEFAULT_CACHE - db = load_db(db_path) - - if args.nid: - unknowns = {nid: "" for nid in args.nid} - else: - unknowns = collect_unknowns() - real_names = collect_real_names() + try: + db = load_db(db_path) + if requested_nids: + unknowns = {nid: "" for nid in (args.nid or [])} + else: + unknowns = collect_unknowns() + real_names = collect_real_names() if unknowns else set() + except (OSError, UnicodeError) as error: + parser.error(str(error)) rows = [] failures = 0 diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 1c7453e93..39711f949 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -1,3 +1,5 @@ +import contextlib +import io import json import tempfile import sys @@ -68,6 +70,27 @@ class DatabaseCacheTests(unittest.TestCase): self.assertEqual(nid_names.load_db(cache), {"nid1": "sceExisting"}) download.assert_not_called() + def test_utf8_bom_does_not_change_the_first_nid(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "aerolib.csv" + nid = nid_names.compute_nid("sceExample") + cache.write_text(f"{nid} sceExample\n", encoding="utf-8-sig") + self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"}) + + def test_comment_rows_do_not_become_database_entries(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "aerolib.csv" + nid = nid_names.compute_nid("sceExample") + cache.write_text(f"# Database names\n{nid} sceExample\n", encoding="utf-8") + self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"}) + + def test_annotations_do_not_change_hash_inputs(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "aerolib.csv" + nid = nid_names.compute_nid("sceExample") + cache.write_text(f"{nid}#A#B sceExample#verified\n", encoding="utf-8") + self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"}) + class NidArgumentTests(unittest.TestCase): options = ["-h", "--help", "--db", "--nid", "--json"] @@ -102,5 +125,82 @@ class NidArgumentTests(unittest.TestCase): self.assertEqual({row["nid"]: row["suggestion"] for row in rows}, database) +class SourceCollectionTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.prx = self.root / "core" / "libs" / "prx" + self.module = self.prx / "libSceExample" + self.module.mkdir(parents=True) + self.root_patch = patch.object(nid_names, "ROOT", self.root) + self.prx_patch = patch.object(nid_names, "PRX", self.prx) + self.root_patch.start() + self.prx_patch.start() + self.addCleanup(self.root_patch.stop) + self.addCleanup(self.prx_patch.stop) + + def write_source(self, text, relative="Export.cpp"): + path = self.module / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def test_comments_and_literals_do_not_create_export_records(self): + self.write_source('''// APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00); +/* int APS5_VABI sceCommented() { return 0; } */ +auto example = R"text(APS5_EXPORT("BBBBBBBBBBB", exampleUnknown01); +int APS5_VABI sceQuoted() { return 0; })text"; +APS5_EXPORT("CCCCCCCCCCC", exampleUnknown02); +int APS5_VABI sceReal() { return 0; } +''') + self.assertEqual(nid_names.collect_unknowns(), {"CCCCCCCCCCC": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown02"}) + self.assertEqual(nid_names.collect_real_names(), {"sceReal"}) + + def test_formatted_alias_macro_is_detected(self): + self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') + self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + + def test_test_fixtures_are_not_runtime_implementations(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nint APS5_VABI sceFixture() { return 0; }\n', "tests/Fixture.cpp") + self.assertEqual(nid_names.collect_unknowns(), {}) + self.assertEqual(nid_names.collect_real_names(), set()) + + def test_unreadable_sources_fail_instead_of_returning_incomplete_names(self): + self.write_source('int APS5_VABI sceReal() { return 0; }\n') + with patch.object(Path, "read_text", side_effect=OSError("read denied")): + with self.assertRaisesRegex(OSError, "read denied"): + nid_names.collect_unknowns() + with self.assertRaisesRegex(OSError, "read denied"): + nid_names.collect_real_names() + + def test_explicit_empty_nid_filter_does_not_scan_the_tree(self): + cache = self.root / "aerolib.csv" + cache.write_text("AAAAAAAAAAA example\n", encoding="utf-8") + output = io.StringIO() + with patch.object(sys, "argv", ["nid_names.py", "--db", str(cache), "--nid", "--json"]), patch.object(nid_names, "collect_unknowns", side_effect=AssertionError("unexpected tree scan")), contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(), 0) + self.assertEqual(json.loads(output.getvalue())["rows"], []) + + def test_empty_abbreviated_filter_skips_all_source_scans(self): + cache = self.root / "aerolib.csv" + cache.write_text("AAAAAAAAAAA example\n", encoding="utf-8") + arguments = ["nid_names.py", "--db", str(cache), "--ni", "--json"] + with patch.object(sys, "argv", arguments), patch.object(nid_names, "collect_unknowns", side_effect=AssertionError("unexpected aliases scan")), patch.object(nid_names, "collect_real_names", side_effect=AssertionError("unexpected definitions scan")), contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(nid_names.main(), 0) + + def test_literals_and_digit_separators_keep_following_definitions(self): + self.write_source('''auto count = 1'000; +auto quote = '\\''; +auto string = "\\\" APS5_VABI sceQuoted() { }"; +int APS5_VABI sceReal() { return 0; } +''') + self.assertEqual(nid_names.collect_real_names(), {"sceReal"}) + + def test_spliced_line_comment_and_macro_tokens(self): + self.write_source('// continued \\\nAPS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nAPS5_EX\\\nPORT("BBBBBBBBBBB", exampleUnknown01);\n') + self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown01"}) + + if __name__ == "__main__": unittest.main() From 83be8dfb2776afac063e961f418d3e0034ef4af4 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:23:13 +0400 Subject: [PATCH 096/277] fix(stubgen): protect input files and keep stdout valid C++ --- tools/stubgen.py | 78 +++++++++++++++++++++----------- tools/tests/test_stubgen.py | 90 +++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 25 deletions(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 5a3f47480..d3c752b53 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -2,6 +2,7 @@ import argparse import re import sys +import tempfile from pathlib import Path try: @@ -27,10 +28,12 @@ def parse_nids(path): nids = [] seen = set() with open(path, "r", encoding="utf-8-sig", errors="replace") as f: - for line in f: + for number, line in enumerate(f, 1): nid = strip_nid_token(line) if not nid: continue + if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: + raise ValueError(f"{path}:{number}: invalid NID {nid!r}") if nid in seen: continue seen.add(nid) @@ -167,6 +170,31 @@ def generate(module, nids, db, td_path=None, td_lib=None): return (code, td_text) +def same_file(left, right): + left, right = Path(left), Path(right) + return left.resolve() == right.resolve() or (left.exists() and right.exists() and left.samefile(right)) + + +def write_outputs(outputs): + staged = [] + try: + for filename, content in outputs: + path = Path(filename).resolve() + if path.is_dir(): + raise IsADirectoryError(str(path)) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as handle: + temporary = Path(handle.name) + staged.append((temporary, path)) + handle.write(content) + if path.exists(): + temporary.chmod(path.stat().st_mode & 0o7777) + for temporary, path in staged: + temporary.replace(path) + finally: + for temporary, path in staged: + temporary.unlink(missing_ok=True) + + def main(argv=None): ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") @@ -175,36 +203,36 @@ def main(argv=None): ap.add_argument("--out", required=False, default=None, help="output file for code, default stdout") ap.add_argument("--td-path", required=False, default=None, help="override file path used inside TechnicalDebt lines, default derived from module, relative to docs/dev") ap.add_argument("--td-lib", required=False, default=None, help="override lib label used inside TechnicalDebt lines, default derived from module") - ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stdout") + ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - nids = parse_nids(args.nids) - db = {} - if args.db: - db = parse_db(args.db) - code, td = generate(args.module, nids, db, args.td_path, args.td_lib) - if args.td_only: + try: + if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): + ap.error("--out and --td-out must name different files") + destinations = [args.td_out] if args.td_only else [args.out, args.td_out] + for destination in filter(None, destinations): + for source in filter(None, [args.nids, args.db]): + if same_file(destination, source): + ap.error(f"output {destination!r} would overwrite input {source!r}") + nids = parse_nids(args.nids) + db = parse_db(args.db) if args.db else {} + code, td = generate(args.module, nids, db, args.td_path, args.td_lib) + outputs = [] + if not args.td_only and args.out: + outputs.append((args.out, code)) if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: + outputs.append((args.td_out, td)) + write_outputs(outputs) + except (OSError, ValueError) as error: + ap.error(str(error)) + if args.td_only: + if not args.td_out: sys.stdout.write(td) return 0 - if args.out: - with open(args.out, "w", encoding="utf-8", newline="\n") as f: - f.write(code) - else: + if not args.out: sys.stdout.write(code) - if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: - if args.out: - sys.stdout.write(td) - else: - if td: - sys.stdout.write("\n") - sys.stdout.write(td) + if not args.td_out: + (sys.stdout if args.out else sys.stderr).write(td) return 0 diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 1ee9c7a47..f8714e203 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,3 +1,6 @@ +import contextlib +import io +import tempfile from pathlib import Path import sys import unittest @@ -25,5 +28,92 @@ class StubgenTests(unittest.TestCase): self.assertIn("NotImplemented_nid_no_patch", code) +class StubGeneratorTests(unittest.TestCase): + def test_default_stdout_contains_only_cpp(self): + with tempfile.TemporaryDirectory() as directory: + nids = Path(directory) / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + output, debt = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(debt): + self.assertEqual(stubgen.main(["--module", "libSceExample", "--nids", str(nids)]), 0) + self.assertTrue(output.getvalue().endswith("\n}\n")) + self.assertNotIn("unknown name, signature", output.getvalue()) + self.assertIn("unknown name, signature", debt.getvalue()) + + def test_output_cannot_overwrite_an_input_file(self): + with tempfile.TemporaryDirectory() as directory: + nids = Path(directory) / "nids.txt" + original = "AAAAAAAAAAA\n" + nids.write_text(original, encoding="utf-8") + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(nids)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(nids.read_text(encoding="utf-8"), original) + + def test_debt_write_failure_preserves_existing_code(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + output.write_text("existing source\n", encoding="utf-8") + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), "existing source\n") + self.assertEqual(sorted(p.name for p in root.iterdir()), ["Export.cpp", "nids.txt"]) + + def test_hard_linked_database_output_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, database, output = root / "nids.txt", root / "names.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "AAAAAAAAAAA sceExample\n" + database.write_text(original, encoding="utf-8") + output.hardlink_to(database) + arguments = ["--module", "libSceExample", "--nids", str(nids), "--db", str(database), "--out", str(output)] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertEqual(database.read_text(encoding="utf-8"), original) + self.assertEqual(output.read_text(encoding="utf-8"), original) + + def test_malformed_nid_is_rejected_before_generating_cpp(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "nids.txt" + path.write_text('AAAAAAAAAAA\ninvalid"nid\n', encoding="utf-8") + with self.assertRaisesRegex(ValueError, "invalid NID"): + stubgen.parse_nids(path) + + def test_code_and_debt_outputs_cannot_overwrite_each_other(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + output = root / "Export.cpp" + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), "--td-out", str(output)] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertFalse(output.exists()) + + def test_valid_annotations_comments_and_duplicates_are_preserved(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "nids.txt" + path.write_text("# imports\nAAAAAAAAAAA#A#B\nAAAAAAAAAAA\nBBBBBBBBBBB\n", encoding="utf-8-sig") + self.assertEqual(stubgen.parse_nids(path), ["AAAAAAAAAAA", "BBBBBBBBBBB"]) + + def test_valid_generation_keeps_the_code_and_debt(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + code, debt = root / "Export.cpp", root / "debt.txt" + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(code), "--td-out", str(debt)] + self.assertEqual(stubgen.main(arguments), 0) + self.assertIn('APS5_EXPORT("AAAAAAAAAAA",', code.read_text(encoding="utf-8")) + self.assertIn("unknown name, signature", debt.read_text(encoding="utf-8")) + + if __name__ == "__main__": unittest.main() From 4abf05234158784a5062e23d353d6c2870c6c135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:23:32 +0200 Subject: [PATCH 097/277] docs(debt): record zero default for unexported pixel inputs --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..e0a42f1f6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -358,6 +358,7 @@ Throughout the project, every function at every stage either **does exactly what - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [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. - [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. - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic From 5ccc11642f9a03eac36fdae1892cc03be89495ab Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:23:59 +0400 Subject: [PATCH 098/277] fix(packaging): validate and stage release archives --- CMakeLists.txt | 2 + tools/package_release.py | 31 +++-- tools/tests/test_package_release.py | 181 ++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 tools/tests/test_package_release.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..0fb42cb0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) + add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) endif() endif() diff --git a/tools/package_release.py b/tools/package_release.py index 53add7365..476314e0e 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -2,15 +2,18 @@ import argparse import re import shutil import tarfile +import tempfile import zipfile from pathlib import Path +ROOT = Path(__file__).resolve().parent.parent + def package(platform, build, output, version): if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): raise ValueError(f"Invalid release tag for asset filenames: {version}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) - expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} + expected = {f"{directory.name}.prx" for directory in (ROOT / "core/libs/prx").iterdir() if directory.is_dir() and not directory.name.startswith(".")} missing = expected - {library.name for library in libraries} if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") @@ -24,25 +27,29 @@ def package(platform, build, output, version): if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - for file in files: - archive.write(file, arcname=f"libs/{file.name}") - with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9) as archive: - for file in files: - archive.add(file, arcname=f"libs/{file.name}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" - shutil.copy2(binary, output / asset) + with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: + staging = Path(temporary) + with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for file in files: + archive.write(file, arcname=f"libs/{file.name}") + with tarfile.open(staging / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9, dereference=True) as archive: + for file in files: + archive.add(file, arcname=f"libs/{file.name}") + shutil.copy2(binary, staging / asset) + for file in sorted(staging.iterdir()): + file.replace(output / file.name) def collect_docs(source, output): - documents = sorted(source.rglob("*.md")) + documents = sorted(document for document in source.rglob("*.md") if document.is_file()) if not documents: raise RuntimeError(f"No Markdown documents found in {source}") - names = set() + names = {file.name.casefold() for file in output.iterdir()} if output.exists() else set() for document in documents: - if document.name in names or (output / document.name).exists(): + if document.name.casefold() in names: raise RuntimeError(f"Duplicate release asset name: {document.name}") - names.add(document.name) + names.add(document.name.casefold()) output.mkdir(parents=True, exist_ok=True) for document in documents: shutil.copy2(document, output / document.name) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py new file mode 100644 index 000000000..c00c6b237 --- /dev/null +++ b/tools/tests/test_package_release.py @@ -0,0 +1,181 @@ +import os +import sys +import tarfile +import tempfile +import unittest +import zipfile +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import package_release + + +@contextmanager +def working_directory(path): + previous = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(previous) + + +class ReleasePackageTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.source = self.root / "source" + self.build = self.root / "build" + self.output = self.root / "output" + (self.source / "core/libs/prx/libc").mkdir(parents=True) + (self.source / "core/libs/prx/libc/Export.cpp").write_text("", encoding="utf-8") + libraries = self.build / "core/libs/libs" + libraries.mkdir(parents=True) + self.library = libraries / "libc.prx" + self.library.write_bytes(b"library bytes") + binary = self.build / "core/relinker/relinker" + binary.parent.mkdir(parents=True) + binary.write_bytes(b"relinker bytes") + self.addCleanup(patch.stopall) + patch.object(package_release, "ROOT", self.source, create=True).start() + + def test_source_discovery_does_not_depend_on_working_directory(self): + with working_directory(self.root): + package_release.package("linux", self.build, self.output, "v1.0") + self.assertEqual((self.output / "relinker-v1.0").read_bytes(), b"relinker bytes") + with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + + def test_hidden_cache_directory_is_not_a_required_library(self): + (self.source / "core/libs/prx/.cache").mkdir() + with working_directory(self.source): + package_release.package("linux", self.build, self.output, "v1.0") + with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: + self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + + def test_tar_and_zip_include_the_same_symlink_contents(self): + target = self.library.with_name("libc.target") + self.library.rename(target) + try: + self.library.symlink_to(target.name) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + with working_directory(self.source): + package_release.package("linux", self.build, self.output, "v1.0") + with tarfile.open(self.output / "prx-linux-v1.0.tar.gz") as archive: + member = archive.getmember("libs/libc.prx") + self.assertTrue(member.isfile()) + self.assertEqual(archive.extractfile(member).read(), b"library bytes") + with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + + def test_missing_real_library_still_fails(self): + self.library.unlink() + with working_directory(self.source), self.assertRaisesRegex(RuntimeError, "Missing patched libraries"): + package_release.package("linux", self.build, self.output, "v1.0") + + def test_invalid_tag_creates_no_assets(self): + with self.assertRaises(ValueError): + package_release.package("linux", self.build, self.output, "../v1.0") + self.assertFalse(self.output.exists()) + + + +class DocumentationAssetsTests(unittest.TestCase): + def test_unique_nested_documents_are_flattened(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "source" + (source / "nested").mkdir(parents=True) + (source / "A.md").write_text("A", encoding="utf-8") + (source / "nested/B.md").write_text("B", encoding="utf-8") + output = Path(tmp) / "assets" + package_release.collect_docs(source, output) + self.assertEqual({file.name: file.read_text(encoding="utf-8") for file in output.iterdir()}, {"A.md": "A", "B.md": "B"}) + + def test_existing_case_collision_preserves_assets(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "source" + source.mkdir() + (source / "guide.md").write_text("new guide", encoding="utf-8") + output = Path(tmp) / "assets" + output.mkdir() + (output / "Guide.md").write_text("old guide", encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "Duplicate release asset"): + package_release.collect_docs(source, output) + self.assertEqual([file.name for file in output.iterdir()], ["Guide.md"]) + self.assertEqual((output / "Guide.md").read_text(encoding="utf-8"), "old guide") + + def test_markdown_directory_is_ignored(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "source" + source.mkdir() + (source / "A.md").write_text("A", encoding="utf-8") + (source / "Z.md").mkdir() + output = Path(tmp) / "assets" + package_release.collect_docs(source, output) + self.assertEqual([file.name for file in output.iterdir()], ["A.md"]) + self.assertEqual((output / "A.md").read_text(encoding="utf-8"), "A") + + def test_case_colliding_assets_fail_before_copying(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "source" + (source / "one").mkdir(parents=True) + (source / "two").mkdir() + (source / "one/Guide.md").write_text("one", encoding="utf-8") + (source / "two/guide.md").write_text("two", encoding="utf-8") + output = Path(tmp) / "assets" + with self.assertRaisesRegex(RuntimeError, "Duplicate release asset"): + package_release.collect_docs(source, output) + self.assertFalse(output.exists()) + + def test_dangling_asset_symlink_is_not_followed(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "source" + source.mkdir() + (source / "Guide.md").write_text("guide", encoding="utf-8") + output = Path(tmp) / "assets" + output.mkdir() + target = Path(tmp) / "outside.md" + try: + (output / "Guide.md").symlink_to(target) + except (OSError, NotImplementedError) as error: + self.skipTest(str(error)) + with self.assertRaisesRegex(RuntimeError, "Duplicate release asset"): + package_release.collect_docs(source, output) + self.assertFalse(target.exists()) + self.assertTrue((output / "Guide.md").is_symlink()) + + +class ReleaseAssetsTests(unittest.TestCase): + def test_failed_tar_creation_preserves_existing_release_assets(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + (source / "core/libs/prx/libc").mkdir(parents=True) + build = root / "build" + (build / "core/libs/libs").mkdir(parents=True) + (build / "core/libs/libs/libc.prx").write_bytes(b"new library") + (build / "core/relinker").mkdir(parents=True) + (build / "core/relinker/relinker").write_bytes(b"new executable") + output = root / "assets" + output.mkdir() + names = ["prx-linux-v1.zip", "prx-linux-v1.tar.gz", "relinker-v1"] + for name in names: + (output / name).write_bytes(b"old " + name.encode()) + previous = Path.cwd() + os.chdir(source) + self.addCleanup(os.chdir, previous) + with patch.object(package_release, "ROOT", source), \ + patch.object(package_release.tarfile, "open", side_effect=OSError("disk full")), \ + self.assertRaisesRegex(OSError, "disk full"): + package_release.package("linux", build, output, "v1") + self.assertEqual(sorted(file.name for file in output.iterdir()), sorted(names)) + for name in names: + self.assertEqual((output / name).read_bytes(), b"old " + name.encode()) + + +if __name__ == "__main__": + unittest.main() From 0ffd244af2f6776b1e0fe8ff7a5bc13a0da29351 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:25:36 +0400 Subject: [PATCH 099/277] fix(ci): preserve test discovery and skip diagnostics --- tools/check_skips.py | 15 ++++--- tools/check_test_registration.py | 60 +++++++++++++++++++++++---- tools/tests/test_check_skips.py | 36 ++++++++++++++++ tools/tests/test_test_registration.py | 48 +++++++++++++++++++++ 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index afe85d4bb..0afad4f27 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -12,13 +12,15 @@ WINDOWS_PREFIX = "agc" def skips(path): for case in ET.parse(path).getroot().iter("testcase"): - if case.find("skipped") is None: + skipped = case.find("skipped") + if skipped is None: continue name = case.get("name") if not name: - continue - output = case.find("system-out") - yield name, (output.text or "") if output is not None else "" + raise ValueError(f"{path}: skipped testcase has no name") + output = ["skipped, " + reason for reason in (skipped.get("message", ""), skipped.text or "") if reason] + output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] + yield name, "\n".join(output) if __name__ == "__main__": @@ -27,7 +29,10 @@ if __name__ == "__main__": parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") args = parser.parse_args() - found = sorted(skips(args.junit)) + try: + found = sorted(skips(args.junit)) + except (OSError, ET.ParseError, ValueError) as error: + parser.error(str(error)) if args.list: for name, output in found: reason = next((line.strip() for line in output.splitlines() if line.startswith("skipped")), "") diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 991cdb59c..83311cb20 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -4,6 +4,8 @@ import subprocess from pathlib import Path TEST_OUTPUT = r"tests[\\/]" +COMMAND = re.compile(r"^\s*(add_test|set_tests_properties)\s*\(", re.M | re.I) +ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) KNOWN_UNRUN = set() @@ -16,10 +18,10 @@ def built(build): path, _, rule = line.partition(": ") if "EXECUTABLE" not in rule: continue - found = re.match(rf"^{TEST_OUTPUT}([A-Za-z0-9_.]+)$", path) + found = re.match(rf"^{TEST_OUTPUT}(?:.*[\\/])?([^\\/]+)$", path) if not found: continue - name = found.group(1).rstrip(".") + name = found.group(1) if name.endswith(".exe"): name = name[:-4] if name: @@ -27,16 +29,58 @@ def built(build): return names +def commands(content): + parsed_until = 0 + for command in COMMAND.finditer(content): + if command.start() < parsed_until: + continue + position, arguments = command.end(), [] + while position < len(content): + if content[position].isspace(): + position += 1 + continue + if content[position] == ")": + parsed_until = position + 1 + yield command.group(1).lower(), arguments + break + if content[position] == "#": + position = content.find("\n", position) + if position < 0: + break + continue + token = ARGUMENT.match(content, position) + if token is None: + raise ValueError(f"invalid CTest argument at position {position}") + argument = token.group(0) + if argument.startswith('"'): + argument = re.sub(r"\\(.)", lambda match: {"n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), argument[1:-1], flags=re.S) + elif token.group("equals") is not None: + width = len(token.group("equals")) + 2 + argument = argument[width:-width] + arguments.append(argument) + position = token.end() + + def registered(build): names = set() for file in Path(build).rglob("CTestTestfile.cmake"): - for line in file.read_text(errors="replace").splitlines(): - found = re.match(r"\s*add_test\s*\((.*)\)\s*$", line) - if not found: + tests, disabled = {}, {} + for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): + if command == "add_test" and arguments: + tests[arguments[0]] = arguments[1:] + elif command == "set_tests_properties" and "PROPERTIES" in arguments: + properties = arguments.index("PROPERTIES") + values = arguments[properties + 1:] + for key, value in zip(values[::2], values[1::2]): + if key == "DISABLED": + for name in arguments[:properties]: + disabled[name] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") + for test, arguments in tests.items(): + if disabled.get(test, False): continue - tokens = re.findall(r'"([^"]*)"|(\S+)', found.group(1)) - for token in tokens[1:]: - argument = token[0] or token[1] + for index, argument in enumerate(arguments): + if index and (argument.startswith("-") or re.match(r"^[A-Za-z_]\w*=", argument)): + continue base = re.split(r"[\\/]", argument)[-1] if base.endswith(".exe"): base = base[:-4] diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index c668ca75d..02ab77f59 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -59,5 +59,41 @@ class CheckSkipsTests(unittest.TestCase): self.assertNotIn("error: geometry skipped", result.stdout) + + +class SkipReportTests(unittest.TestCase): + def skipped(self, content): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "ctest.xml" + path.write_text(content, encoding="utf-8") + return list(check_skips.skips(path)) + + def test_skip_message_preserves_the_device_reason(self): + found = self.skipped('') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_stderr_preserves_the_device_reason(self): + found = self.skipped('skipped, no display') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_unnamed_skipped_test_is_an_error(self): + with self.assertRaises(ValueError): + self.skipped('') + + def test_unexpected_reason_stays_unexpected(self): + found = self.skipped('') + self.assertIsNone(check_skips.DEVICE_LIMITATION.search(found[0][1])) + + def test_ctest_stdout_is_retained(self): + found = self.skipped('skipped, the device reports too few descriptors') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_message_and_element_text_are_both_retained(self): + found = self.skipped('the device has no required extension') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 4010ca6bd..5a4649281 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -2,6 +2,8 @@ from pathlib import Path import sys import tempfile import unittest +from types import SimpleNamespace +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_test_registration @@ -32,6 +34,52 @@ class RegistrationTests(unittest.TestCase): self.assertIn("buffer_bounds_tests", names) self.assertNotIn("missing_tests", names) + def test_hyphenated_executable_is_discovered(self): + output = "tests/buffer-bounds_tests: CXX_EXECUTABLE_LINKER\n" + with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)): + self.assertEqual(check_test_registration.built("build"), {"buffer-bounds_tests"}) + + def test_nested_test_output_is_discovered(self): + output = "tests/gpu/buffer_bounds_tests: CXX_EXECUTABLE_LINKER\n" + with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)): + self.assertEqual(check_test_registration.built("build"), {"buffer_bounds_tests"}) + + def test_quoted_cmake_path_is_unescaped(self): + names = self.registered(r'add_test("bounds" "/build/tests/a\"b_tests")' + '\n') + self.assertIn('a"b_tests', names) + + def test_bracket_quoted_command_is_recognized(self): + names = self.registered('add_test([=[bounds]=] [=[/build/tests/buffer_bounds_tests]=])\n') + self.assertIn("buffer_bounds_tests", names) + + def test_multiline_test_command_is_recognized(self): + names = self.registered('add_test("bounds"\n "/build/tests/buffer_bounds_tests"\n)\n') + self.assertIn("buffer_bounds_tests", names) + + def test_disabled_test_is_not_counted_as_run(self): + names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\nset_tests_properties("bounds" PROPERTIES DISABLED TRUE)\n') + self.assertNotIn("buffer_bounds_tests", names) + + def test_option_value_does_not_register_executable(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" "runner.py" "--report=/build/tests/missing_tests")\n') + self.assertNotIn("missing_tests", names) + + def test_reenabled_test_is_counted_as_run(self): + names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\nset_tests_properties("bounds" PROPERTIES DISABLED TRUE)\nset_tests_properties("bounds" PROPERTIES DISABLED OFF)\n') + self.assertIn("buffer_bounds_tests", names) + + def test_disabled_wrapper_does_not_register_its_argument(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" "runner.py" "/build/tests/buffer_bounds_tests")\nset_tests_properties([=[bounds]=] PROPERTIES DISABLED "ON")\n') + self.assertNotIn("buffer_bounds_tests", names) + + def test_command_text_inside_an_argument_is_not_another_test(self): + names = self.registered('add_test("bounds" "/usr/bin/python3" [=[\nadd_test("fake" "/build/tests/missing_tests")\n]=])\n') + self.assertNotIn("missing_tests", names) + + def test_windows_escaped_path_is_decoded(self): + names = self.registered(r'add_test("bounds" "C:\\build\\tests\\buffer_bounds_tests.exe")' + '\n') + self.assertIn("buffer_bounds_tests", names) + if __name__ == "__main__": unittest.main() From 27599ccfbf82c27d3b724a1d09365040f91c3d90 Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Sun, 11 Oct 2026 23:32:41 +0900 Subject: [PATCH 100/277] fix(libc): align mspace allocations to 32 bytes Clang's PS4 and PS5 targets set the default new alignment and the suitable alignment to 32 bytes, so PS5 code may store to a fresh mspace block with 32-byte-aligned AVX instructions, as the guest heap already allows since #1621. The mspace allocator aligned blocks to 16 bytes only. Every mspace allocation is now aligned to at least 32 bytes. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Mspace.cpp | 3 ++- core/libs/tests/Mspace.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index d64d02d4a..16e7c5792 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -16,6 +16,7 @@ namespace { constexpr unsigned MspaceThreadUnsafe = 1; constexpr std::uintptr_t Granule = 16; +constexpr std::uintptr_t MinimumAlignment = 32; constexpr std::uintptr_t ArenaHeaderBytes = 64; struct Chunk { @@ -60,7 +61,7 @@ void RemoveFree(Arena& arena, std::map::iterator chunk) { void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (!arena) return nullptr; - alignment = std::max(alignment, Granule); + alignment = std::max(alignment, MinimumAlignment); const auto needed = AlignUp(std::max(size, 1), Granule); if (needed < size || needed > std::numeric_limits::max() - alignment) { Error(12); diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2d61f1c56..dc3d03d87 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -34,7 +34,33 @@ struct MallocManagedSize { static void Require(bool value) { if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } } +static void CheckAlignment() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); + Require(arena != nullptr); + const auto aligned = [](const void* pointer) { return pointer && (reinterpret_cast(pointer) & 31u) == 0; }; + void* blocks[64] = {}; + for (std::size_t index = 0; index < 64; ++index) { + blocks[index] = sceLibcMspaceMalloc_nid_postfix(arena, index + 1); + Require(aligned(blocks[index])); + std::memset(blocks[index], 0xa5, index + 1); + } + void* zeroed = sceLibcMspaceCalloc_nid_postfix(arena, 3, 13); + Require(aligned(zeroed)); + blocks[0] = sceLibcMspaceRealloc_nid_postfix(arena, blocks[0], 600); + Require(aligned(blocks[0])); + void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40); + Require(aligned(small)); + void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 40, 8); + Require(aligned(moved)); + void* posix = nullptr; + Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &posix, 8, 40) == 0 && aligned(posix)); + for (void* block : blocks) sceLibcMspaceFree_nid_postfix(arena, block); + for (void* block : {zeroed, small, moved, posix}) sceLibcMspaceFree_nid_postfix(arena, block); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} int main() { + CheckAlignment(); alignas(16) std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("test", storage.data(), storage.size(), 0); Require(arena != nullptr); From d7f1a6d3d5584431b0f11da6bbdb032509c9aae1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:32:44 +0000 Subject: [PATCH 101/277] fix(libkernel): translate host errno in the file functions' SCE errors SceErrorFromErrno in the libkernel file functions passed host errno values 1-34 through and turned every other value into EIO. Linux numbers EAGAIN 11, which is FreeBSD's EDEADLK, so a read from an empty non-blocking pipe returned SCE_KERNEL_ERROR_EDEADLK, and ENAMETOOLONG, ELOOP, ENOTEMPTY, EDQUOT and EOVERFLOW, whose host values differ from FreeBSD's, all became EIO. Map those host values to the guest's FreeBSD numbers, as SceIoErrorFromErrno in Open.cpp already does for EAGAIN, EDEADLK, EDQUOT and EOVERFLOW. The guest constants the callers pass are all at most 34 and never 11, or GUEST_ENOTEMPTY, so they are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel/File/src/Stdio.cpp | 22 +++++++++++++++++++++- core/libs/tests/GuestFileVector.cpp | 7 +++++++ core/libs/tests/GuestFilesystem.cpp | 5 +++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..77b99cea3 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -282,6 +282,10 @@ static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; static constexpr int GUEST_ENOTEMPTY = 66; +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 int SceErrorFromErrno(int error); @@ -297,7 +301,23 @@ static bool FailPosixPositionedGuestSocket(int descriptor) { } static int SceErrorFromErrno(int error) { - return static_cast(0x80020000u | static_cast(error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO)); + int guest = error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO; + if (error == EAGAIN) guest = GUEST_EWOULDBLOCK; +#ifdef EDEADLK + else if (error == EDEADLK) guest = GUEST_EDEADLK; +#endif + else if (error == ENAMETOOLONG) guest = GUEST_ENAMETOOLONG; + else if (error == ENOTEMPTY) guest = GUEST_ENOTEMPTY; +#ifdef ELOOP + else if (error == ELOOP) guest = GUEST_ELOOP; +#endif +#ifdef EDQUOT + else if (error == EDQUOT) guest = GUEST_EDQUOT; +#endif +#ifdef EOVERFLOW + else if (error == EOVERFLOW) guest = GUEST_EOVERFLOW; +#endif + return static_cast(0x80020000u | static_cast(guest)); } extern "C" int* APS5_VABI __error_nid_postfix(); diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 5bfffc521..370be6be8 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -16,6 +16,7 @@ static int MakePipe(int* ends) { return ::_pipe(ends, 64, _O_BINARY); } static int ClosePipe(int end) { return ::_close(end); } #else +#include #include static int MakePipe(int* ends) { return ::pipe(ends); } static int ClosePipe(int end) { return ::close(end); } @@ -59,6 +60,7 @@ static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); static constexpr std::int64_t ErrorEspipe = static_cast(0x8002001Du); +static constexpr std::int64_t ErrorEagain = static_cast(0x80020023u); static std::string Contents(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); @@ -206,6 +208,11 @@ int main() { Require(std::memcmp(first, "xyz", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); +#ifndef _WIN32 + Require(::fcntl(ends[0], F_SETFL, ::fcntl(ends[0], F_GETFL) | O_NONBLOCK) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == ErrorEagain); + Require(readv_nid_postfix(ends[0], reads, 2) == -1 && *__error_nid_postfix() == 35); +#endif Require(ClosePipe(ends[0]) == 0 && ClosePipe(ends[1]) == 0); const int random = sceKernelOpen("/dev/urandom", 0, 0); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..2cc3fefb7 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -500,6 +500,11 @@ int main() { Require(remove_nid_postfix(dotMissingName.c_str()) == -1 && *__error_nid_postfix() == 2); const auto missingParentLeaf = rootName + "/missing/leaf"; Require(sceKernelMkdir(missingParentLeaf.c_str(), 0700) == static_cast(0x80020002u)); +#ifndef _WIN32 + const auto longLeaf = rootName + "/" + std::string(300, 'n'); + Require(sceKernelMkdir(longLeaf.c_str(), 0700) == static_cast(0x8002003fu)); + Require(mkdir_nid_postfix(longLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == 63); +#endif const auto invalidNameUnderFile = presentName + "/a:b/x"; Require(mkdir_nid_postfix(invalidNameUnderFile.c_str(), 0700) == -1 && *__error_nid_postfix() == 20); Require(sceKernelMkdir(invalidNameUnderFile.c_str(), 0700) == static_cast(0x80020014u)); From b0d7d8254fb712b923349e4b477b98acb061f8ce Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 12:44:39 +0000 Subject: [PATCH 102/277] fix(shader): handle NaN below positive infinity --- .../SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dc3fa2a70..17d3ff48d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1131,6 +1131,13 @@ std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + if (arg1 == ConstantF32(state, 0x7f800000u)) { + const auto bits = EmitNative(state, arg0); + const auto magnitude = EmitNative(state, bits, ConstantU32(state, 0x7fffffffu)); + const auto ordered = EmitNative(state, magnitude, ConstantU32(state, 0x7f800000u)); + const auto notPositiveInfinity = EmitNative(state, bits, ConstantU32(state, 0x7f800000u)); + return EmitNative(state, ordered, notPositiveInfinity); + } return EmitNative(state, arg0, arg1); } From d668b7c2b1e2ce5ba1f01eb9204ee502bc60ea57 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 11 Oct 2026 15:33:31 +0200 Subject: [PATCH 103/277] fix(shader): check NaN on the bits for ordered less-than and greater-than On the AMD Windows driver 32.0.13058.8001 (Radeon 860M), a NaN compared less than +infinity, or +infinity greater than a NaN, gives 1 when the infinity reaches the shader as a constant, as in agc_driver_f32_compare_constant's register operand case (#3536). The same driver returns 1 for a NaN in an ordered not-equal (#825), which is presumably what its compiler turns these compares into. FPOrdLessThan32 and FPOrdGreaterThan32 now AND their result with the integer NaN check FPOrdNotEqual32 already uses, which gives the same result for every input. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 1cfbde5d69fe3b5c0aa482d4ff3551f0e87c3915) --- .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 20 ++++++++----------- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 17d3ff48d..1d17f82ac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1117,13 +1117,16 @@ std::uint32_t EmitFPUnordEqual32(SpirvEmitterState& state, std::uint32_t arg0, s return EmitNative(state, arg0, arg1); } -std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { +static std::uint32_t BothOrderedBits32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { const auto ordered = [&](std::uint32_t value) { const auto magnitude = EmitNative(state, EmitNative(state, value), ConstantU32(state, 0x7fffffffu)); return EmitNative(state, magnitude, ConstantU32(state, 0x7f800000u)); }; - const auto bothOrdered = EmitNative(state, ordered(arg0), ordered(arg1)); - return EmitNative(state, EmitNative(state, arg0, arg1), bothOrdered); + return EmitNative(state, ordered(arg0), ordered(arg1)); +} + +std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1131,14 +1134,7 @@ std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - if (arg1 == ConstantF32(state, 0x7f800000u)) { - const auto bits = EmitNative(state, arg0); - const auto magnitude = EmitNative(state, bits, ConstantU32(state, 0x7fffffffu)); - const auto ordered = EmitNative(state, magnitude, ConstantU32(state, 0x7f800000u)); - const auto notPositiveInfinity = EmitNative(state, bits, ConstantU32(state, 0x7f800000u)); - return EmitNative(state, ordered, notPositiveInfinity); - } - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1146,7 +1142,7 @@ std::uint32_t EmitFPUnordLessThan32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..cbc314d3c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -482,7 +482,7 @@ Throughout the project, every function at every stage either **does exactly what - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. -- [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of the compare feeds a ballot on a 32-wide subgroup (#825) +- [Ordered not-equal, less-than and greater-than compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run - 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. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. From abf6f06be4186af06dae726972d26883d8623ff0 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:37:32 +0200 Subject: [PATCH 104/277] fix(rudp): provide the calls PPSA03490 still lacks PPSA03490 (Demon Slayer) imports sceRudpGetMaxSegmentSize, sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived, which no library exports. The first three answer by the library's initialised state; sceRudpNetReceived is declared without an implementation because its signature is unknown. Adds one test and TechnicalDebt entries. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/CMakeLists.txt | 10 ++++++ core/libs/prx/libSceRudp/Export.cpp | 27 +++++++++++++++ core/libs/tests/GuestRudpPump.cpp | 53 +++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 ++ 4 files changed, 93 insertions(+) create mode 100644 core/libs/tests/GuestRudpPump.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..82d36c2e1 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -208,6 +208,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_pump_tests tests/GuestRudpPump.cpp) + target_include_directories(guest_rudp_pump_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_pump_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_pump_tests) + add_test(NAME guest_rudp_pump COMMAND guest_rudp_pump_tests) + if(WIN32) + set_tests_properties(guest_rudp_pump PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 132ea291f..06a56008a 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -242,4 +242,31 @@ int APS5_VABI sceRudpRead(int ctx_id, void* data, uint32_t len, int flags, void* } return valid_ctx(ctx_id) ? RUDP_ERROR_CONN_RESET : RUDP_ERROR_INVALID_CONTEXT_ID; } + +int APS5_VABI sceRudpNetFlush(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpProcessEvents(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpNetReceived(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { + if (size == nullptr) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return 0; +} + } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp new file mode 100644 index 000000000..999a4cf13 --- /dev/null +++ b/core/libs/tests/GuestRudpPump.cpp @@ -0,0 +1,53 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpProcessEvents(); +int APS5_VABI sceRudpNetFlush(); +int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); +int APS5_VABI sceRudpNetReceived(); +} + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "rudp check failed at line %d\n", line); + std::abort(); + } +} +#define CHECK(value) Require((value), __LINE__) + +int main() { + constexpr int notInitialized = static_cast(0x80770001); + constexpr int invalidArgument = static_cast(0x80770004); + std::uint16_t segmentSize = 0x5a5a; + CHECK(sceRudpProcessEvents() == notInitialized); + CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); + CHECK(segmentSize == 0x5a5a); + + alignas(8) static unsigned char pool[0xC58]; + CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + CHECK(sceRudpProcessEvents() == 0); + CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); + CHECK(segmentSize == 0x5a5a); + CHECK(sceRudpTerminate() == 0); + + try { + sceRudpNetReceived(); + } catch (const std::runtime_error& error) { + CHECK(std::string(error.what()) == "sceRudpNetReceived not implemented"); + return 0; + } + std::fprintf(stderr, "sceRudpNetReceived returned instead of reporting that it is not implemented\n"); + return 1; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..b32c54ad8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -597,3 +597,6 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- PPSA03490 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceRudpNetReceived](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) +- [sceRudpProcessEvents and sceRudpNetFlush](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, so with nothing to process or send they return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown +- [sceRudpGetMaxSegmentSize](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - PPSA03490 passes a pointer to a 16-bit value it has set to 0 and treats a nonzero result as failure. The real segment size is unknown, so nothing is written and the title keeps 0; it returns `RUDP_ERROR_INVALID_ARGUMENT` for a null pointer, `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit` and 0 after it From 5ea9ce2e40df9eeacf4e60702e45b256f3cabb96 Mon Sep 17 00:00:00 2001 From: Oguzhan Ozdemir <48760055+oguzhanozfe@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:21:04 +0300 Subject: [PATCH 105/277] fix(relinker): allow identical Windows function export aliases --- core/relinker/CMakeLists.txt | 14 ++++ .../src/windows/WindowsGuestModuleWriter.cpp | 9 ++- .../include/relinker/guest/GuestImage.hpp | 10 ++- .../relinker/src/guest/GuestImageReader.cpp | 11 ++- .../relinker/src/guest/GuestModuleBuilder.cpp | 2 +- .../tests/WindowsExportAliasTests.cpp | 75 +++++++++++++++++++ .../tests/test_windows_export_aliases.py | 47 ++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 core/relinker/relinker/tests/WindowsExportAliasTests.cpp create mode 100644 core/relinker/relinker/tests/test_windows_export_aliases.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..8a0ed0d66 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -81,6 +81,20 @@ target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs ) +if(BUILD_TESTING AND WIN32) + get_target_property(windowsAliasTestSources relinker SOURCES) + list(REMOVE_ITEM windowsAliasTestSources + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp) + add_test_executable(windows_guest_export_alias_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp + ${windowsAliasTestSources}) + get_target_property(windowsAliasIncludes relinker INCLUDE_DIRECTORIES) + target_include_directories(windows_guest_export_alias_tests PRIVATE ${windowsAliasIncludes}) + add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) +endif() + if(WIN32) target_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index f7ca50fa2..bcc3469dd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -120,9 +120,16 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI return false; }; std::map exports; + std::map exportSymbols; PeSection tlsExports{".tlsrefs", nextRva, SectionRead | 0x40u, {}}; for (const auto& symbol : guest.Symbols) { - if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + const auto [previous, inserted] = exportSymbols.emplace(symbol.Name, &symbol); + if (!inserted) { + if (!Relinker::IdenticalOrdinaryFunctionExport(*previous->second, symbol)) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + continue; + } + if (symbol.Section == Relinker::AbsoluteSection) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); std::uint32_t rva = 0; if ((symbol.Info & 15) == 6) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index fcc35ac65..d566a152c 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -25,8 +25,16 @@ struct GuestSymbol { std::uint64_t Value; std::uint64_t Size; std::string Library; + std::string QualifiedName; }; +inline bool IdenticalOrdinaryFunctionExport(const GuestSymbol& left, const GuestSymbol& right) { + return left.Section != 0 && left.Section < 0xff00 && (left.Info >> 4) != 0 && + left.Visibility != 1 && left.Visibility != 2 && (left.Info & 15) == 2 && + left.Info == right.Info && left.Visibility == right.Visibility && + left.Section == right.Section && left.Value == right.Value && left.Size == right.Size; +} + struct GuestImage { std::filesystem::path SourcePath; std::string OutputName; @@ -49,7 +57,7 @@ struct GuestImage { class GuestImageReader { public: std::vector ReadModuleNames(const std::vector& bytes) const; - GuestImage Read(const std::filesystem::path& path, std::vector bytes) const; + GuestImage Read(const std::filesystem::path& path, std::vector bytes, bool windows = false) const; }; struct GuestArtifact { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index ac7caaf6b..e6c3545a7 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -87,7 +87,7 @@ std::vector GuestImageReader::ReadModuleNames(const std::vector bytes) const { +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const { const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) { @@ -204,19 +204,21 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::vector neededLibraries; for (const auto offset : needed) neededLibraries.push_back(string(offset)); - std::set exports; + std::map exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; if (Io::ReadU16(bytes, symOffset + offset + 6) == 0 && library.empty()) { if (auto declared = Domain::DeclaredImportModule(name, importModules); !declared.empty()) image.Dynamic.UnmatchedImportModules.insert(std::move(declared)); } + const auto qualifiedName = name; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; const auto section = Io::ReadU16(bytes, symOffset + offset + 6); const auto value = Io::ReadU64(bytes, symOffset + offset + 8); const auto size = Io::ReadU64(bytes, symOffset + offset + 16); + const GuestSymbol symbol{name, info, visibility, section, value, size, library, qualifiedName}; if (offset == 0 && (info != 0 || visibility != 0 || section != 0 || value != 0 || size != 0 || !name.empty())) fail("Invalid null symbol"); if (offset == 0) { image.Symbols.push_back({{}, 0, 0, 0, 0, 0}); @@ -224,7 +226,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); if (section != 0 && (info >> 4) != 0 && visibility != 1 && visibility != 2) { - if (name.empty() || !exports.insert(name).second) fail("Duplicate or empty export after stripping #: " + name); + const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); + if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); } if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { @@ -234,7 +237,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size, library}); + image.Symbols.push_back(symbol); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 689f4dafd..e2c63cd97 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -122,7 +122,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; }); }; - for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)))); + for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); diff --git a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp new file mode 100644 index 000000000..78cc146c8 --- /dev/null +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Bytes = std::vector; +static void Check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); } +static void Ph(Bytes& b, int i, unsigned type, unsigned flags, unsigned off, unsigned va, unsigned size) { + const auto p=64+i*56; Io::WriteU32(b,p,type); Io::WriteU32(b,p+4,flags); + Io::WriteU64(b,p+8,off);Io::WriteU64(b,p+16,va);Io::WriteU64(b,p+24,va); + Io::WriteU64(b,p+32,size);Io::WriteU64(b,p+40,size);Io::WriteU64(b,p+48,8); +} +static Bytes Fixture(bool reverse=false) { + Bytes b(0x4000);std::memcpy(b.data(),"\x7f" "ELF\x02\x01\x01",7); + Io::WriteU16(b,16,3);Io::WriteU16(b,18,62);Io::WriteU32(b,20,1); + Io::WriteU64(b,32,64);Io::WriteU16(b,52,64);Io::WriteU16(b,54,56);Io::WriteU16(b,56,3); + Ph(b,0,1,5,0x1000,0,0x1000);Ph(b,1,1,6,0x2000,0x2000,0x1000); + b[0x1020]=0xc3;b[0x1028]=0xc3; + std::string strings(1,'\0');auto str=[&](const char* s){auto o=strings.size();strings+=s;strings+='\0';return o;}; + const auto a=str(reverse?"alias#B#A":"alias#A#A"),c=str(reverse?"alias#A#A":"alias#B#A"); + const auto imp=str("imported#A#B"),host=str("fixtureHost.prx"),module=str("fixtureModule"); + std::copy(strings.begin(),strings.end(),b.begin()+0x2200); + const std::pair tags[]={{5,0x2200},{10,strings.size()},{6,0x2400},{11,24},{4,0x2600},{7,0x2700},{8,48},{9,24},{1,host},{0x61000043,module},{0x61000045,(1ull<<48)|host},{0,0}}; + Ph(b,2,2,6,0x2000,0x2000,sizeof(tags)); + for(unsigned i=0;i=start&&rva-start static void Reject(F&& f,const char* expected) { + bool rejected=false;try{f();}catch(const Domain::RelinkerException& e){rejected=std::string(e.what()).find(expected)!=std::string::npos;} + Check(rejected,expected); +} +static void Positive(bool reverse) { + const auto bytes=Fixture(reverse);const auto g=Relinker::GuestImageReader().Read("generic.prx",bytes,true); + Check(g.Symbols.size()==4&&g.Bytes==bytes,"input symbols/bytes changed"); + Check(g.Symbols[1].Name=="alias"&&g.Symbols[2].Name=="alias","stripped lookup names"); + Check(g.Symbols[1].QualifiedName==(reverse?"alias#B#A":"alias#A#A")&&g.Symbols[2].QualifiedName==(reverse?"alias#A#A":"alias#B#A"),"qualified names lost"); + Check(g.Symbols[1].Info==18&&g.Symbols[2].Info==18&&g.Symbols[1].Visibility==3&&g.Symbols[2].Visibility==3&&g.Symbols[1].Section==1&&g.Symbols[2].Section==1&&g.Symbols[1].Value==0x20&&g.Symbols[2].Value==0x20&&g.Symbols[1].Size==1&&g.Symbols[2].Size==1,"raw tuple changed"); + Check((Io::ReadU64(g.Dynamic.RelaData,8)>>32)==1&&(Io::ReadU64(g.Dynamic.RelaData,32)>>32)==2,"alias relocation indices changed"); + Check(g.Symbols[3].QualifiedName=="imported#A#B"&&g.Symbols[3].Library=="fixtureHost.prx","qualified import changed"); + Domain::GuestRuntime runtime;const auto pe=Elfpatcher::GuestModuleWriter().WriteWindows(g,runtime); + const auto directory=Offset(pe,Io::ReadU32(pe,Io::ReadU32(pe,0x3c)+24+112)); + Check(Io::ReadU32(pe,directory+20)==1&&Io::ReadU32(pe,directory+24)==1,"multiple PE exports"); + const auto function=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+28))); + const auto nameRva=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+32))); + Check(std::strcmp(reinterpret_cast(pe.data()+Offset(pe,nameRva)),"alias")==0,"wrong export name"); + Elfpatcher::Windows::WindowsLoadImage image(g.Bytes,g.Headers,false); + Check(function==image.GetRva(0x20,1),"wrong export RVA"); + for(auto target:{0x2800,0x2808})Check(Io::ReadU64(pe,Offset(pe,image.GetRva(target,8)))==Elfpatcher::Windows::ImageBase+function,"wrong alias relocation value"); +} +int main() { + try { + Positive(false);Positive(true); + Reject([]{Relinker::GuestImageReader().Read("generic.prx",Fixture());},"Duplicate or empty export"); + const std::array,7> changes={{{4,0x22},{4,0x11},{5,0},{6,2},{8,0x28},{16,2},{6,Relinker::AbsoluteSection}}}; + for(const auto& [field,value]:changes){auto b=Fixture();auto off=0x2400+48+field;if(field<6)b[off]=value;else if(field==6)Io::WriteU16(b,off,value);else Io::WriteU64(b,off,value);Reject([&]{Relinker::GuestImageReader().Read("generic.prx",b,true);},"Duplicate or empty export");} + auto good=Relinker::GuestImageReader().Read("generic.prx",Fixture(),true); + for(unsigned kind=0;kind<10;++kind){auto g=good;auto& s=g.Symbols[2];switch(kind){case 0:s.Value+=8;break;case 1:s.Size+=1;break;case 2:s.Info=0x22;break;case 3:s.Info=0x11;break;case 4:s.Visibility=0;break;case 5:s.Section=2;break;case 6:g.Symbols[1].Info=s.Info=0x11;break;case 7:g.Symbols[1].Section=s.Section=Relinker::AbsoluteSection;break;case 8:g.Symbols[1].Info=s.Info=0x16;g.Dynamic.RelaData.clear();break;case 9:g.Symbols[1].Info=s.Info=0x10;break;}Check(!Relinker::IdenticalOrdinaryFunctionExport(g.Symbols[1],s),"nonordinary/conflicting tuple accepted");Domain::GuestRuntime r;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(g,r);},kind==8?"TLS":"Duplicate guest export");} + Check(Domain::DeclaredImportModule("same#A#B",{{1,"one"},{2,"two"}})=="one"&&Domain::DeclaredImportModule("same#A#C",{{1,"one"},{2,"two"}})=="two","module-qualified IDs merged"); + std::cout<<"WINDOWS_EXPORT_ALIAS_CASES_PASSED\npreserved-indices reversed-order conflicting-fields strict-nonwindows qualified-import writer-guard\n"; + } catch(const std::exception& e){std::cerr< Date: Sun, 11 Oct 2026 16:43:10 +0200 Subject: [PATCH 106/277] test(relinker): drop unregistered evidence script for export aliases The script needs a stock and a patched converter binary as arguments and is not run by ctest; the C++ test covers the same cases. --- .../tests/test_windows_export_aliases.py | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 core/relinker/relinker/tests/test_windows_export_aliases.py diff --git a/core/relinker/relinker/tests/test_windows_export_aliases.py b/core/relinker/relinker/tests/test_windows_export_aliases.py deleted file mode 100644 index 86f886c73..000000000 --- a/core/relinker/relinker/tests/test_windows_export_aliases.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Generic converter regression; emitted guest PE is inspected, never executed.""" -from pathlib import Path -import hashlib,json,struct,subprocess,sys -sys.path.insert(0,str(Path(__file__).resolve().parent)) -from test_windows_import_modules import executable - -def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() -def fixture(): - b=bytearray(0x4000);b[:7]=b'\x7fELF\x02\x01\x01' - struct.pack_into(' Date: Mon, 12 Oct 2026 02:49:47 +1300 Subject: [PATCH 107/277] fix(agc): leave the depth surface out when no enabled test has its plane DecodeState bound the depth surface whenever DB_DEPTH_CONTROL enabled a depth, stencil or depth bounds test and either plane was valid. A stencil test beside a depth-only surface (DB_STENCIL_INFO format invalid, depth test off) tests nothing, but the surface was still attached and the render extent was clamped to it. A draw into a 1920x1080 target with a 960x540 depth surface left bound then covered only the top-left quarter. A test whose plane is present but invalid no longer binds the surface. Such a draw renders without a depth attachment, as it already does when both planes are invalid. DrawRejection follows the same rule. --- .../libSceAgcDriver/Graphics/src/State.cpp | 18 +++++++++++---- .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cf0d6b1ea..189a01c25 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -138,6 +138,14 @@ bool depthSurfaceBound(const Registers& cx) { return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); } +std::uint32_t planeTests(const Registers& cx, std::uint32_t depthControl) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + if (z != cx.end() && (z->second & 3u) == 0) depthControl &= ~2u; + if (stencil != cx.end() && (stencil->second & 1u) == 0) depthControl &= ~1u; + return depthControl & 0xbu; +} + bool depthPlanesAbsent(const Registers& cx) { const auto z = find(cx, 0x010); const auto stencil = find(cx, 0x011); @@ -583,9 +591,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; - if (clear || ((depthControl & 0xbu) != 0 && depthSurfaceBound(cx))) { + const auto tests = planeTests(cx, depthControl); + if (clear || (tests != 0 && depthSurfaceBound(cx))) { decodeDepth(cx, depthControl, result); - } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && depthPlanesAbsent(cx))) { + } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { reported = true; @@ -1008,8 +1017,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t z = 0, view = 0; if ((value(cx, 0x010, z) && (z & 3u) == 0) || (value(cx, 0x002, view) && (view & 0x01000000u) != 0)) return require(false, "depth clear requires a writable depth plane"); } - const bool surface = ((word & 0xbu) != 0 || depthClear || stencilClear) && depthSurfaceBound(cx); - if (!surface && !((word & 0xbu) != 0 && depthPlanesAbsent(cx)) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + const auto tests = planeTests(cx, word); + const bool surface = (tests != 0 || depthClear || stencilClear) && depthSurfaceBound(cx); + if (!surface && !((word & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx))) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 8u) == 0 || surface || depthPlanesAbsent(cx), "depth bounds without a depth surface"); !reason.empty()) return reason; if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..d9a9eb7fc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -275,6 +275,29 @@ void stateTests() { queue.context[0x200] = 0x007007bbu; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bounds without a depth plane"); queue = makeState(); + queue.context[0x010] = 0x80000183; + queue.context[0x011] = 0x20000180; + queue.context[0x012] = 0x00001000; + queue.context[0x014] = 0x00001000; + queue.context[0x007] = 0x0001001f; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0x3f800000; + queue.context[0x200] = 0x007007b1; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x00700231; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a failing stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a failing stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x007007b3; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depth.has_value() && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth test on a bound depth plane lost its surface"); + queue = makeState(); queue.context[0x10f] = 0x7fc00000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite"); // The register facade: every register the decoders read is in DrawKeyRegisters (a wrong hit of From 5bd43e3e8ef8af626060ccc437095259fcf14ade Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:55:03 +0200 Subject: [PATCH 108/277] build(libc): register filesystem statistics source separately --- core/libs/prx/libSceLibcInternal/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index f3f1f0cdd..4c2d3367e 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,7 +1,8 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp UserContext.cpp FilesystemStatistics.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) configure_windows_unwind(libSceLibcInternal) +target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp) if(BUILD_TESTING) add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) From bd0998a7a8cdff68a2a785676658b3c04abb7d63 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:56:57 +0000 Subject: [PATCH 109/277] fix(build): keep libc.prx needed by every executable that links a prx Ubuntu's GCC links with --as-needed, so a test that calls nothing in libc.prx directly dropped it from DT_NEEDED and satisfied the prx libraries' __cxa_* and __gxx_personality_v0 references from the static libstdc++ instead. Those copies interpose libc.prx's, so exceptions thrown inside the prx libraries were searched by libgcc and resumed by the guest unwinder, which aborted guest_kernel_stack_protect. libc now propagates to every consumer of a prx with --no-as-needed on Linux. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/CMakeLists.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..17384eb28 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -55,6 +55,7 @@ target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) configure_windows_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) + target_link_options(libc INTERFACE LINKER:--no-as-needed) endif() set(UNPATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs/unpatched) @@ -101,9 +102,9 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() if(LIB_NAME STREQUAL "libkernel") - target_link_libraries(${LIB_NAME} PRIVATE libc) + target_link_libraries(${LIB_NAME} PUBLIC libc) else() - target_link_libraries(${LIB_NAME} PRIVATE libkernel libc) + target_link_libraries(${LIB_NAME} PRIVATE libkernel PUBLIC libc) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(${LIB_NAME} PRIVATE LINKER:-z,now) From 4337b43f54bc5183700a18063c9fd09ebffe7119 Mon Sep 17 00:00:00 2001 From: Tharun Balaji Date: Sun, 11 Oct 2026 18:06:32 +0530 Subject: [PATCH 110/277] fix(libSceHttp2): validate SSL setter handles --- core/libs/prx/libSceHttp2/Export.cpp | 99 +++++++++++++++++++++------- core/libs/tests/GuestHttp2.cpp | 56 ++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 132 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index e4869b148..29b452b27 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -6,19 +6,54 @@ #include "prx/libkernel/Equeue/Equeue.hpp" #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(0x80436063); static constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); static constexpr int ERROR_NULL_POINTER = static_cast(0x817B1225); + +enum class Http2Kind : std::uint8_t { Context, Template, Request, CookieBox }; + +struct Http2Handle { + Http2Kind kind; + int owner; + std::size_t poolSize; +}; + static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +static std::mutex g_handlesMutex; +static std::unordered_map g_handles; + +static Http2Handle* FindHandle(int id, Http2Kind kind) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() && entry->second.kind == kind ? &entry->second : nullptr; +} + +static Http2Handle* FindAnyHandle(int id) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() ? &entry->second : nullptr; +} + +static void ReleaseOwnedHandles(int owner) { + std::vector owned; + for (const auto& entry : g_handles) { + if (entry.second.owner == owner) owned.push_back(entry.first); + } + for (const int id : owned) { + ReleaseOwnedHandles(id); + g_handles.erase(id); + } +} + +static bool HandleIsLive(int id) { + std::lock_guard lock(g_handlesMutex); + return FindAnyHandle(id) != nullptr; +} struct Http2MemoryPoolStats { size_t pool_size; @@ -58,29 +93,43 @@ int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* val } int APS5_VABI sceHttp2CreateRequestWithURL(int tmpl_id, const char* method, const char* url, uint64_t content_length) { - (void)tmpl_id; (void)method; (void)url; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::Request, tmpl_id, 0}; + return id; } int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)lib_http2_ctx_id; (void)user_agent; (void)http_ver; (void)is_auto_proxy_conf; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::Template, lib_http2_ctx_id, 0}; + return id; } int APS5_VABI sceHttp2DeleteRequest(int req_id) { + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(req_id); + } std::lock_guard lock(g_completionsMutex); g_completions.erase(req_id); return 0; } int APS5_VABI sceHttp2DeleteTemplate(int tmpl_id) { - (void)tmpl_id; + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + ReleaseOwnedHandles(tmpl_id); + g_handles.erase(tmpl_id); return 0; } @@ -109,8 +158,8 @@ int APS5_VABI sceHttp2Init(int libnet_mem_id, int libssl_ctx_id, size_t pool_siz (void)libssl_ctx_id; (void)max_concurrent_request; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; + std::lock_guard lock(g_handlesMutex); + g_handles[id] = {Http2Kind::Context, 0, pool_size}; return id; } @@ -219,26 +268,25 @@ int APS5_VABI sceHttp2SetTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttp2SslDisableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SslEnableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SetMinSslVersion(int id, uint32_t ssl_version) { - (void)id; (void)ssl_version; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(lib_http2_ctx_id); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + ReleaseOwnedHandles(lib_http2_ctx_id); + g_handles.erase(lib_http2_ctx_id); return 0; } @@ -260,13 +308,13 @@ int APS5_VABI sceHttp2AbortRequest(int req_id) { int APS5_VABI sceHttp2GetMemoryPoolStats(int lib_http2_ctx_id, Http2MemoryPoolStats* stats) { if (stats == nullptr) return ERROR_NULL_POINTER; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(lib_http2_ctx_id); - if (pool == g_pools.end()) { + std::lock_guard lock(g_handlesMutex); + const Http2Handle* handle = FindHandle(lib_http2_ctx_id, Http2Kind::Context); + if (handle == nullptr) { std::memset(stats, 0, sizeof(*stats)); return ERROR_INVALID_ID; } - *stats = {pool->second, 0, 0, 0}; + *stats = {handle->poolSize, 0, 0, 0}; return 0; } @@ -276,8 +324,11 @@ int APS5_VABI sceHttp2CookieFlush(int id) { } int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { - (void)lib_http2_ctx_id; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::CookieBox, lib_http2_ctx_id, 0}; + return id; } int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index 4bfb83c4f..b766155e3 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -15,6 +15,10 @@ int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); int APS5_VABI sceHttp2SendRequestAsync(int, const void*, std::size_t, Http2AsyncOption*, void*); int APS5_VABI sceHttp2WaitAsync(int, Http2AsyncResult*, std::uint32_t*, void*); int APS5_VABI sceHttp2DeleteRequest(int); +int APS5_VABI sceHttp2DeleteTemplate(int); +int APS5_VABI sceHttp2SetMinSslVersion(int, std::uint32_t); +int APS5_VABI sceHttp2SslEnableOption(int, std::uint32_t); +int APS5_VABI sceHttp2SslDisableOption(int, std::uint32_t); int APS5_VABI sceHttp2Term(int); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); @@ -40,6 +44,14 @@ int APS5_VABI sceHttp2WebSocketSetPingInterval(); static void Require(bool value) { if (!value) std::abort(); } +static constexpr int InvalidId = static_cast(0x817B1100); + +static void RequireSslSetters(int id, int expected) { + Require(sceHttp2SetMinSslVersion(id, 0) == expected); + Require(sceHttp2SslEnableOption(id, 0) == expected); + Require(sceHttp2SslDisableOption(id, 0) == expected); +} + template static bool Throws(TCall call) { try { @@ -72,6 +84,50 @@ int main() { Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + RequireSslSetters(0, InvalidId); + RequireSslSetters(-1, InvalidId); + RequireSslSetters(0x7fffffff, InvalidId); + RequireSslSetters(context, 0); + RequireSslSetters(box, 0); + RequireSslSetters(other, 0); + RequireSslSetters(tmpl, 0); + RequireSslSetters(request, 0); + + const int doomedRequest = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/doomed", 0); + Require(doomedRequest > 0); + Require(sceHttp2DeleteRequest(doomedRequest) == 0); + RequireSslSetters(doomedRequest, InvalidId); + Require(sceHttp2DeleteRequest(doomedRequest) == InvalidId); + + const int doomedTemplate = sceHttp2CreateTemplate(context, "agent", 2, 0); + Require(doomedTemplate > 0); + const int doomedTemplateRequest = sceHttp2CreateRequestWithURL(doomedTemplate, "GET", "https://example.com/child", 0); + Require(doomedTemplateRequest > 0); + Require(sceHttp2DeleteTemplate(doomedTemplate) == 0); + RequireSslSetters(doomedTemplate, InvalidId); + RequireSslSetters(doomedTemplateRequest, InvalidId); + Require(sceHttp2DeleteTemplate(doomedTemplate) == InvalidId); + + const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(doomedContext > 0); + const int doomedContextBox = sceHttp2CreateCookieBox(doomedContext); + const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); + Require(doomedContextBox > 0 && doomedContextTemplate > 0); + const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); + Require(doomedContextRequest > 0); + RequireSslSetters(doomedContext, 0); + Require(sceHttp2Term(doomedContext) == 0); + RequireSslSetters(doomedContext, InvalidId); + RequireSslSetters(doomedContextBox, InvalidId); + RequireSslSetters(doomedContextTemplate, InvalidId); + RequireSslSetters(doomedContextRequest, InvalidId); + Require(sceHttp2Term(doomedContext) == InvalidId); + + const int freshContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(freshContext > 0); + RequireSslSetters(freshContext, 0); + Require(sceHttp2Term(freshContext) == 0); + KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "http2") == 0); Require(sceKernelAddUserEventEdge(eq, request) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..b35c64b71 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -36,7 +36,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output - [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` fails and the other exports throw. PPSA01317 skips its narration set-up when initialization fails - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. -- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) +- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified From c42614371237dc064e3a38f45e190fccd76d7d49 Mon Sep 17 00:00:00 2001 From: Tharun Balaji Date: Sun, 11 Oct 2026 20:22:04 +0530 Subject: [PATCH 111/277] fix(libSceHttp2): clear completion queues for cascade-deleted requests sceHttp2DeleteRequest erased the request's completion queue, but sceHttp2DeleteTemplate and sceHttp2Term removed descendant request handles through ReleaseOwnedHandles without clearing their completion queues. A request that completed asynchronously before its template or context was deleted kept a stale completion that sceHttp2WaitAsync could still consume. ReleaseOwnedHandles now returns every transitively removed handle id, and DeleteTemplate and Term erase the completion queue of each removed request. CompleteAsync validates that the request is live while holding the handle lock and inserts the completion under it, so a send that races a deletion either returns the invalid-id error without queuing or is erased by the deletion; a completion is no longer re-created after a deletion returns. The only lock nesting stays handles before completions, and no mutex is held while the user event is triggered. GuestHttp2 covers a queued completion before sceHttp2DeleteTemplate and before sceHttp2Term: the completion is no longer consumable, a late send returns the invalid-id error without queuing, and the removed handles are rejected. --- core/libs/prx/libSceHttp2/Export.cpp | 69 +++++++++++++++++++--------- core/libs/tests/GuestHttp2.cpp | 54 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 29b452b27..f037bf7f9 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -39,15 +39,19 @@ static Http2Handle* FindAnyHandle(int id) { return entry != g_handles.end() ? &entry->second : nullptr; } -static void ReleaseOwnedHandles(int owner) { +static std::vector ReleaseOwnedHandles(int owner) { std::vector owned; for (const auto& entry : g_handles) { if (entry.second.owner == owner) owned.push_back(entry.first); } + std::vector removed; for (const int id : owned) { - ReleaseOwnedHandles(id); + const std::vector descendants = ReleaseOwnedHandles(id); + removed.insert(removed.end(), descendants.begin(), descendants.end()); g_handles.erase(id); + removed.push_back(id); } + return removed; } static bool HandleIsLive(int id) { @@ -65,21 +69,36 @@ struct Http2MemoryPoolStats { static std::mutex g_completionsMutex; static std::unordered_map> g_completions; -static void CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { +static void EraseCompletions(const std::vector& ids) { + std::lock_guard lock(g_completionsMutex); + for (const int id : ids) g_completions.erase(id); +} + +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; { - std::lock_guard lock(g_completionsMutex); - Http2AsyncResult completion{}; - completion.req_id = req_id; - completion.result = ERROR_NETWORK; - g_completions[req_id].push_back(completion); + std::lock_guard handlesLock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) != nullptr) { + std::lock_guard completionsLock(g_completionsMutex); + Http2AsyncResult completion{}; + completion.req_id = req_id; + completion.result = ERROR_NETWORK; + g_completions[req_id].push_back(completion); + queued = true; + } } + if (!queued) return ERROR_INVALID_ID; 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); - g_completions[req_id].pop_back(); + { + std::lock_guard lock(g_completionsMutex); + const auto pending = g_completions.find(req_id); + if (pending != g_completions.end() && !pending->second.empty()) pending->second.pop_back(); + } NotImplemented_nid_no_patch(function); } + return 0; } extern "C" { @@ -120,16 +139,19 @@ int APS5_VABI sceHttp2DeleteRequest(int req_id) { if (FindHandle(req_id, Http2Kind::Request) == nullptr) return ERROR_INVALID_ID; g_handles.erase(req_id); } - std::lock_guard lock(g_completionsMutex); - g_completions.erase(req_id); + EraseCompletions({req_id}); return 0; } int APS5_VABI sceHttp2DeleteTemplate(int tmpl_id) { - std::lock_guard lock(g_handlesMutex); - if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; - ReleaseOwnedHandles(tmpl_id); - g_handles.erase(tmpl_id); + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(tmpl_id); + g_handles.erase(tmpl_id); + } + EraseCompletions(removed); return 0; } @@ -189,8 +211,7 @@ int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) { (void)post_data; (void)size; - CompleteAsync(__func__, req_id, kqueue_option, option); - return 0; + return CompleteAsync(__func__, req_id, kqueue_option, option); } int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) { @@ -283,10 +304,14 @@ int APS5_VABI sceHttp2SetMinSslVersion(int id, uint32_t ssl_version) { } int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { - std::lock_guard lock(g_handlesMutex); - if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; - ReleaseOwnedHandles(lib_http2_ctx_id); - g_handles.erase(lib_http2_ctx_id); + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(lib_http2_ctx_id); + g_handles.erase(lib_http2_ctx_id); + } + EraseCompletions(removed); return 0; } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index b766155e3..e64d3053f 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -154,4 +154,58 @@ int main() { Require(sceHttp2DeleteRequest(request) == 0); Require(sceKernelDeleteEqueue(eq) == 0); Require(sceHttp2Term(context) == 0); + + KernelEqueue templateEqueue = 0; + Require(sceKernelCreateEqueue(&templateEqueue, "http2-template") == 0); + const int cascadeContext = sceHttp2Init(1, 1, 0x3000, 4); + Require(cascadeContext > 0); + const int cascadeTemplate = sceHttp2CreateTemplate(cascadeContext, "agent", 2, 0); + Require(cascadeTemplate > 0); + const int cascadeRequest = sceHttp2CreateRequestWithURL(cascadeTemplate, "GET", "https://example.com/cascade", 0); + Require(cascadeRequest > 0); + Require(sceKernelAddUserEventEdge(templateEqueue, cascadeRequest) == 0); + Http2AsyncOption cascadeOption{}; + cascadeOption.equeue = templateEqueue; + cascadeOption.user_event_id = cascadeRequest; + cascadeOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == 0); + KernelEvent cascadeEvent{}; + int cascadeCount = 0; + Require(sceKernelWaitEqueue(templateEqueue, &cascadeEvent, 1, &cascadeCount, &timeout) == 0 && cascadeCount == 1); + Require(sceHttp2DeleteTemplate(cascadeTemplate) == 0); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(cascadeRequest) == InvalidId); + RequireSslSetters(cascadeRequest, InvalidId); + Require(sceHttp2Term(cascadeContext) == 0); + Require(sceKernelDeleteUserEvent(templateEqueue, cascadeRequest) == 0); + Require(sceKernelDeleteEqueue(templateEqueue) == 0); + + KernelEqueue termEqueue = 0; + Require(sceKernelCreateEqueue(&termEqueue, "http2-term") == 0); + const int terminatedContext = sceHttp2Init(1, 1, 0x4000, 4); + Require(terminatedContext > 0); + const int terminatedTemplate = sceHttp2CreateTemplate(terminatedContext, "agent", 2, 0); + Require(terminatedTemplate > 0); + const int terminatedRequest = sceHttp2CreateRequestWithURL(terminatedTemplate, "GET", "https://example.com/terminated", 0); + Require(terminatedRequest > 0); + Require(sceKernelAddUserEventEdge(termEqueue, terminatedRequest) == 0); + Http2AsyncOption termOption{}; + termOption.equeue = termEqueue; + termOption.user_event_id = terminatedRequest; + termOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == 0); + KernelEvent termEvent{}; + int termCount = 0; + Require(sceKernelWaitEqueue(termEqueue, &termEvent, 1, &termCount, &timeout) == 0 && termCount == 1); + Require(sceHttp2Term(terminatedContext) == 0); + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(terminatedRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(terminatedRequest) == InvalidId); + Require(sceHttp2DeleteTemplate(terminatedTemplate) == InvalidId); + RequireSslSetters(terminatedContext, InvalidId); + RequireSslSetters(terminatedRequest, InvalidId); + Require(sceKernelDeleteUserEvent(termEqueue, terminatedRequest) == 0); + Require(sceKernelDeleteEqueue(termEqueue) == 0); } From 9758607197c525700ac483aa3cbd312e9e5b649c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 16:57:18 +0200 Subject: [PATCH 112/277] docs(technical-debt): record invalid-id returns of the other Http2 handle exports --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b35c64b71..2ae18c6c3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -36,7 +36,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output - [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` fails and the other exports throw. PPSA01317 skips its narration set-up when initialization fails - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. -- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused +- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL`, `sceHttp2CreateCookieBox` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified From f942de360ec1159db4a764f25ca5dd5065b291be Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 14:58:39 +0000 Subject: [PATCH 113/277] test(libc): keep Windows unwind coverage within guest frames --- core/libs/tests/WindowsExceptions.cpp | 27 +++++++++++++-------------- docs/dev/TechnicalDebt.md | 1 + 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 3b5bc6116..e09d897de 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -156,25 +156,24 @@ static void testUncaughtExceptions() { if (seen != 1) throw std::runtime_error("unwinding did not count its exception"); } -static int onceDestroyed; -struct OnceGuard { ~OnceGuard() { ++onceDestroyed; } }; -static void ThrowThroughOnce(int depth) { - OnceGuard guard; - if (depth) { ThrowThroughOnce(depth - 1); return; } - std::once_flag flag; - std::call_once(flag, [] { throw std::runtime_error("through runtime frames"); }); +static int guestFrameDestroyed; +struct GuestFrameGuard { ~GuestFrameGuard() { ++guestFrameDestroyed; } }; +static void ThrowThroughGuestFrames(int depth) { + GuestFrameGuard guard; + if (depth) { ThrowThroughGuestFrames(depth - 1); return; } + throw std::runtime_error("through guest frames"); } -static void testUnwindThroughRuntimeModules() { +static void testUnwindThroughGuestFrames() { for (int depth : {0, 1, 7, 64}) { - onceDestroyed = 0; + guestFrameDestroyed = 0; try { - ThrowThroughOnce(depth); - throw std::logic_error("call_once did not propagate"); + ThrowThroughGuestFrames(depth); + throw std::logic_error("guest frame did not propagate"); } catch (const std::runtime_error& error) { - if (std::strcmp(error.what(), "through runtime frames")) throw; + if (std::strcmp(error.what(), "through guest frames")) throw; } - if (onceDestroyed != depth + 1) throw std::runtime_error("unwinding through runtime DLL frames skipped a destructor"); + if (guestFrameDestroyed != depth + 1) throw std::runtime_error("unwinding through guest frames skipped a destructor"); } } @@ -184,7 +183,7 @@ int main() { testFutureException(); testMadeExceptionPointer(); testUncaughtExceptions(); - testUnwindThroughRuntimeModules(); + testUnwindThroughGuestFrames(); try { Rethrow(); return 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..af04129b1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -395,6 +395,7 @@ Throughout the project, every function at every stage either **does exactly what - [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection +- The Windows libc exception unwinder reads the project's `.ehmeta` and `.ehfram` tables, not PE `.pdata`/`.xdata`. WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll` have empty `.eh_frame` sections, so guest exceptions cannot currently cross their frames (for example, the `std::call_once` path through `pthread_once`); `RtlVirtualUnwind` stepping and Windows cleanup handlers are not implemented - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask From 54e5aad214fea6e77aa08db2e7d4ac70303404e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:59:10 +0000 Subject: [PATCH 114/277] test(libkernel): skip the read-only truncate cases when running as root Root ignores the file mode, so truncating a file with only the owner read bit succeeds and guest_truncate aborted when the suite ran as root, as it does in containers. The read-only cases now run only for other users and the test says on stderr when it skips them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/tests/GuestTruncate.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestTruncate.cpp b/core/libs/tests/GuestTruncate.cpp index 57f387546..0f8d4b1e5 100644 --- a/core/libs/tests/GuestTruncate.cpp +++ b/core/libs/tests/GuestTruncate.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -10,6 +11,8 @@ #define NOMINMAX #endif #include +#else +#include #endif extern "C" { @@ -74,8 +77,12 @@ int main() { Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); #else std::filesystem::permissions(file, std::filesystem::perms::owner_read); - RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); - Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + if (::geteuid() != 0) { + RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + } else { + std::fprintf(stderr, "running as root, which ignores file permissions: the read-only cases not tested\n"); + } std::filesystem::permissions(file, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); #endif Require(Contents(file) == "locked"); From c81ee9288c268801837244cf0cda5d73b6f3f749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 17:01:55 +0200 Subject: [PATCH 115/277] docs(technical-debt): record fstatvfs deviations from FreeBSD --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..1c1970921 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -595,5 +595,6 @@ Throughout the project, every function at every stage either **does exactly what - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [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. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields +- [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) - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. From 199225225fbb44ae47329fcf1a269a83a6c3dcef Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:02:49 +0100 Subject: [PATCH 116/277] test(shader): cover SRT read use patching --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..692461f52 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -994,12 +994,14 @@ if(BUILD_TESTING) endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) + set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) 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}") if(WIN32) - set_tests_properties(agc_shader_bindless 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 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_constant_bit_test_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 ${agcExecutionTests}) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f..0aac83323 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -9,6 +9,8 @@ #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -2655,6 +2657,36 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } +void verifySrtReadPatching() { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + program.Resources().memoryInfo.push_back(memory); + + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& address = builder.Emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&builder.Constant(0u), &builder.Constant(0u)}); + auto& active = program.CreateValue(IrOpcode::Void, IrType::U1); + active.SetImmediateBool(true); + auto& read = builder.Emit(IrOpcode::LoadAddressU32, IrType::U32, {&address, &builder.Constant(0u), &builder.Constant(0u), &active}, MemoryFlags{0u, 0u}); + auto& repeatedUse = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&read, &read}); + auto& secondUser = builder.Emit(IrOpcode::IMul32, IrType::U32, {&read, &builder.Constant(2u)}); + + SrtWalker{}.BuildPlan(program); + + require(program.Resources().srtReads.size() == 1u, "SRT read patching: the scalar read was not planned"); + const auto* replacement = repeatedUse.Argument(0); + require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); + require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const auto& remainingUses = read.OperandUses(); + require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); +} + struct NestedRequest { std::vector code; std::array userData{}; @@ -2925,6 +2957,11 @@ int main(int argc, char** argv) { std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { + verifySrtReadPatching(); + std::cout << "SRT read patching replaces repeated and multiple uses\n"; + return 0; + } require(argc == 1, "unknown shader memory test arguments"); verifyRegisterSources(); verifyEvaluatedValues(); @@ -2956,6 +2993,7 @@ int main(int argc, char** argv) { verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); verifyFunctionLdsBound(); + verifySrtReadPatching(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); #if ANYPS5_ENABLE_SPIRV_TOOLS From 9ac583bdddd0f97beaf7b212e91a4ed7f5e1208e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:46:29 +0200 Subject: [PATCH 117/277] feat(libSceAudioIn): implement sceAudioInHqOpen and sceAudioInAsyncOpen Both open the same SDL capture port as sceAudioInOpen, so Input, GetSilentState and Close work on their handles. HqOpen accepts only a 128-sample grain at 48 kHz (SharpProspero, KytyPS5); AsyncOpen returns INVALID_SIZE above 384 samples (SharpProspero). Values the sources disagree on throw. Format 0x10 still throws: only KytyPS5 accepts it. Imported by FMOD-based titles, The Smurfs Dreams (PPSA21607) and Astro Bot (PPSA21564); Ghost of Yotei (PPSA26344) calls sceAudioInOpen. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 16bfdf928d0a2da0b4473db2633a2e270538cbf5) --- core/libs/prx/libSceAudioIn/Export.cpp | 79 ++++++++++++++------------ core/libs/tests/GuestAudioIn.cpp | 59 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 50e917ee5..b3ae47b9f 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -21,6 +21,7 @@ constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast(0x80260107); constexpr int AUDIO_IN_ERROR_BUSY = static_cast(0x8026010A); constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1; constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4; +constexpr std::uint32_t MAX_ASYNC_LEN = 384; using Clock = std::chrono::steady_clock; @@ -75,6 +76,32 @@ Port* find(int handle) { return port.used ? &port : nullptr; } +int checkArguments(std::uint32_t type, std::uint32_t index, std::uint32_t len, std::uint32_t freq) { + if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; + if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; + if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; + if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; + return 0; +} + +int openPort(std::uint32_t len, std::uint32_t freq, std::uint32_t param, const char* function) { + if (param == 0x10) { + NotImplemented_nid_no_patch(function); + return 0; + } + Format format{}; + if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; + std::lock_guard lock(g_mutex); + for (std::size_t i = 0; i < g_ports.size(); ++i) { + if (g_ports[i].used) continue; + const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); + const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); + g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; + return static_cast(i + 1); + } + return AUDIO_IN_ERROR_PORT_FULL; +} + std::size_t capture(SDL_AudioDeviceID device, std::uint8_t* dest, std::size_t bytes, Clock::time_point deadline) { while (SDL_GetQueuedAudioSize(device) < bytes && Clock::now() < deadline) PreciseSleepUs(1000); const std::size_t queued = SDL_GetQueuedAudioSize(device); @@ -136,25 +163,8 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { int APS5_VABI sceAudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { (void)user_id; - if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; - if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; - if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; - if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; - if (param == 0x10) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - Format format{}; - if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; - std::lock_guard lock(g_mutex); - for (std::size_t i = 0; i < g_ports.size(); ++i) { - if (g_ports[i].used) continue; - const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); - const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); - g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; - return static_cast(i + 1); - } - return AUDIO_IN_ERROR_PORT_FULL; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInClose(int32_t handle) { @@ -168,25 +178,24 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { } int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + if (len != 128 || freq != 48000) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + const int error = checkArguments(type, index, len, freq); + if (error == AUDIO_IN_ERROR_INVALID_SIZE && len <= MAX_ASYNC_LEN) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + if (error != 0) return error; + return openPort(len, freq, param, __func__); } APS5_EXPORT("X+4jdIS75P0", sceAudioInUnknown_X4jdIS75P0); diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index f3b3d3ff4..d9e957911 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -6,12 +7,15 @@ #include #include #include +#include #include #include extern "C" { int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -19,6 +23,16 @@ int APS5_VABI sceAudioInClose(int); static void Require(bool value) { if (!value) std::abort(); } +template +static bool ReportsNotImplemented(const char* name, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == std::string(name) + " not implemented"; + } + return false; +} + static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -42,6 +56,22 @@ void TestValidation() { Require(sceAudioInOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 3) == invalidParam); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(sceAudioInHqOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInHqOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInHqOpen(user, 0, 0, 512, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 128, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, 3) == invalidParam); + Require(sceAudioInAsyncOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInAsyncOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInAsyncOpen(user, 0, 0, 385, 48000, 2) == invalidSize); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 256, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 16000, 2); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInOpen", [] { sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); })); } void TestCapture() { @@ -66,6 +96,11 @@ void TestCapture() { Require(sceAudioInInput(handle, block.data()) == 256); for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); Require(sceAudioInClose(handle) == 0); + const int async = sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 2); + Require(async == handle && sceAudioInGetSilentState(async) == 0); + Require(sceAudioInInput(async, block.data()) == 256); + Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); + Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); } @@ -87,8 +122,30 @@ void TestNoDevice() { for (std::size_t i = 0; i < 256 * 8; ++i) Require(buffer[i] == 0); Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); - for (int port = 1; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); + const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 0x11); + Require(hq > 0 && async > 0 && hq != handle && async != handle && hq != async); + Require(sceAudioInGetSilentState(hq) == 1 && sceAudioInGetSilentState(async) == 1); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(hq, buffer.data()) == 128); + for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[128 * 4] == 0xCC); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(async, buffer.data()) == 256); + for (std::size_t i = 0; i < 256 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[256 * 4] == 0xCC); + Require(sceAudioInInput(async, nullptr) == 0); + for (int port = 3; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInClose(hq) == 0); + Require(sceAudioInClose(hq) == invalidHandle); + Require(sceAudioInGetSilentState(hq) == invalidHandle); + Require(sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2) == hq); + Require(sceAudioInClose(async) == 0); + Require(sceAudioInInput(async, buffer.data()) == invalidHandle); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 0x12) == async); Require(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..ad4ce77b0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -345,6 +345,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior 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. - [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. From 61d4494863d15dffc1a007052ba3354a17e14cc9 Mon Sep 17 00:00:00 2001 From: goncalompontes Date: Sun, 11 Oct 2026 16:03:46 +0100 Subject: [PATCH 118/277] fix(shader): accept the inline constant third source of VOP3 interpolation The assembler encodes the unused third source of the VOP3-encoded interpolation forms as the inline constant 0 (src2 = 0x80, bit 25 of the second word), which the decoder refused. Accept that encoding and refuse any other third source. The decode test now uses the assembler words for every accepted form and keeps the refusals one field away from a valid word. --- .../tests/Vop3InterpDecode.cpp | 34 +++++++++---------- .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 6 +++- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp index a8c267b53..b3a4aca23 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -34,25 +34,25 @@ struct Refused { int main() { constexpr std::uint32_t Destination = 5; const std::vector accepted{ - {{0xd6000005u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u}, - {{0xd6010005u, 0x00020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u}, - {{0xd6020005u, 0x00000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u}, - {{0xd6000005u, 0x0002041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u}, - {{0xd6000005u, 0x00020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u}, - {{0xd6000005u, 0x0003fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u}, - {{0xd6000205u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u}, - {{0xd6000005u, 0x40020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, - {{0xd6008005u, 0x00020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, - {{0xd6000005u, 0x08020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, + {{0xd6000005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u}, + {{0xd6010005u, 0x02020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u}, + {{0xd6020005u, 0x02000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0202041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x02020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0203fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u}, + {{0xd6000205u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u}, + {{0xd6000005u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, + {{0xd6008005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, + {{0xd6000005u, 0x0a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, }; const std::vector refused{ - {{0xd6000005u, 0x00000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, - {{0xd6020005u, 0x00000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"}, - {{0xd6000805u, 0x00020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"}, - {{0xd6000005u, 0x00020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, - {{0xd6000005u, 0x00060400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with a third source"}, - {{0xd6000105u, 0x00020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, - {{0xd6020005u, 0x20000000u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + {{0xd6000005u, 0x02000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, + {{0xd6020005u, 0x02000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"}, + {{0xd6000005u, 0x02060400u}, "third source is not the inline constant 0", "v_interp_p1_f32_e64 with a third source other than the inline constant 0"}, + {{0xd6000805u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"}, + {{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"}, }; int failures = 0; for (const auto& test : accepted) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 00b7078af..5856ec94e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2123,7 +2123,11 @@ bool isVop3F32InterpolationOpcode(RdnaOpcode opcode) { void decodeVop3F32Interpolation(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 moves = instruction.op == RdnaOpcode::VInterpMovF32; - if ((abs & 0x5u) != 0u || (neg & 0x5u) != 0u || opSel != 0u || src2 != 0u || (src0 >> 8u) != 0u || (moves && (abs | neg) != 0u)) { + const RdnaOperand thirdSource = DecodeRdnaScalarSource(src2, instruction.programCounter); + if (thirdSource.kind != RdnaOperandKind::IntegerInlineConstant || thirdSource.value != 0u) { + throw std::invalid_argument("VOP3 interpolation third source is not the inline constant 0"); + } + if ((abs & 0x5u) != 0u || (neg & 0x5u) != 0u || opSel != 0u || (src0 >> 8u) != 0u || (moves && (abs | neg) != 0u)) { throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); } if (moves) { From 9ff132d9a589c340244a91537bc3deff68f481e3 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:04:38 -0400 Subject: [PATCH 119/277] fix(shader): accept the clamp bit on VOP3 float compares The decoder throws "VOP3 source modifiers are not implemented" for VOP3-encoded float compares (v_cmp_*_f16/f32/f64) that set the clamp bit, so shaders using them fail to compile. On RDNA2 hardware the clamp bit does not change the result of a float compare: the measured results with and without it are the same. The decoder now accepts the bit on float compares; clamp on other carry-out and scalar-destination opcodes still throws. Co-Authored-By: Claude Haiku 5.5 --- .../tests/execution/VCmpEqF16Clamp.cpp | 143 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 2 +- 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp new file mode 100644 index 000000000..5f6fcd90d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t VccLow = 106u; +constexpr std::uint32_t VectorSourceBase = 256u; +constexpr std::uint32_t ShaderCmpEqF16 = 0xcau; +constexpr std::uint32_t ShaderCmpLeF64 = 0x23u; +constexpr std::uint32_t ShaderCmpNgtF64 = 0x2bu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct Row { + std::array sources; + std::uint32_t expected; +}; + +struct Vop3CompareWords { + std::uint32_t word0; + std::uint32_t word1; +}; + +Vop3CompareWords Vop3Compare(std::uint32_t opcode, std::uint32_t source0, std::uint32_t source1) { + const std::uint32_t encoded0 = VectorSourceBase + source0; + const std::uint32_t encoded1 = VectorSourceBase + source1; + return {0xd4000000u | (opcode << 16u) | VccLow | 0x8000u, encoded0 | (encoded1 << 9u)}; +} + +std::vector ClampCode(const Vop3CompareWords& compare) { + return { + 0x34020082u, 0x34060082u, + 0xe0302000u, 0x80000401u, + 0xe0302004u, 0x80000501u, + 0xe0302008u, 0x80000601u, + 0xe030200cu, 0x80000701u, + 0xbf8c3f70u, + compare.word0, compare.word1, + 0xd5010014u, 0x01a90280u, + 0xe0702000u, 0x80011403u, + 0xbf810000u, + }; +} + +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}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void RunClampRows(AgcDriver::VulkanDevice& device, const char* name, const Vop3CompareWords& compare, + const ShaderRecompiler::ShaderFloatMode& floatMode, const std::vector& rows) { + Input.fill(0u); + for (std::size_t index = 0; index < rows.size(); ++index) { + std::copy(rows[index].sources.begin(), rows[index].sources.end(), + Input.begin() + static_cast(index * Inputs)); + } + 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 auto code = ClampCode(compare); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.context.floatMode = floatMode; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(words.data())); + device.WaitIdle(); + for (std::size_t index = 0; index < rows.size(); ++index) { + const auto actual = Output[index * Results]; + Require(actual == rows[index].expected, std::string(name) + " clamp row " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(rows[index].expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const ShaderRecompiler::ShaderFloatMode denormalsKept{0xf0u, true, true, false}; + const ShaderRecompiler::ShaderFloatMode denorm32Flushed{0xc0u, true, false, false}; + RunClampRows(*device, "v_cmp_eq_f16_e64 vcc_lo, v4, v5", Vop3Compare(ShaderCmpEqF16, 4u, 5u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x0000bc00u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00003555u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x0000fc00u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00007c01u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00006400u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0xbeef8000u, 0xbeefbc00u, 0xbeef0000u, 0x00000000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_le_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpLeF64, 4u, 6u), denorm32Flushed, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000000u}, + {{0x00000000u, 0xfff00000u, 0x00000000u, 0x40040000u}, 0x00000001u}, + {{0x00000001u, 0x7ff00000u, 0x00000000u, 0x41e00000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_ngt_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpNgtF64, 4u, 6u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000001u}, + }); + std::puts("v_cmp clamp 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 0e6078d53..08c232192 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2016,7 +2016,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, if (carryInOut || scalarDst) { const bool floatCompare = scalarDst && isVopcFloatCompareOpcode(opcode); const bool divScale = opcode == RdnaOpcode::VDivScaleF32 || opcode == RdnaOpcode::VDivScaleF64; - if ((clamp != 0u && !isVop3BMadU64Opcode(opcode)) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { + if ((clamp != 0u && !isVop3BMadU64Opcode(opcode) && !floatCompare) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; From 3c6c5d53316fbed9fdbaa73b9ad5bd0eb76fe1f2 Mon Sep 17 00:00:00 2001 From: Zurisen Date: Sun, 11 Oct 2026 12:04:09 +0200 Subject: [PATCH 120/277] fix(libkernel): reserve the AMM ranges below host regions at the top of the guest arena The AMM ranges were reserved at the last 64 GiB of the guest arena and the reservation threw when that window was not free. On Windows the stacks of the threads that exist before the arena is reserved (the main thread and the loader's workers) are placed at random addresses and are left in the arena as host regions; when one fell in the last 64 GiB, the first AMM use threw "fixed guest mapping ... overlaps a host region in the guest arena". Reserve the highest free 64 GiB window instead: the last 64 GiB when it is free, and the window just below an occupied part of it otherwise. --- core/libs/prx/libSceAmpr/CMakeLists.txt | 10 ++++++ core/libs/prx/libc/include/GuestArena.hpp | 1 + core/libs/prx/libc/src/GuestArena.cpp | 19 +++++++++++ core/libs/prx/libkernel/Apr/src/Apr.cpp | 5 ++- core/libs/tests/GuestAmmPlacement.cpp | 40 +++++++++++++++++++++++ core/libs/tests/GuestAmpr.cpp | 2 +- 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestAmmPlacement.cpp diff --git a/core/libs/prx/libSceAmpr/CMakeLists.txt b/core/libs/prx/libSceAmpr/CMakeLists.txt index 09a966fea..896ffaf20 100644 --- a/core/libs/prx/libSceAmpr/CMakeLists.txt +++ b/core/libs/prx/libSceAmpr/CMakeLists.txt @@ -14,6 +14,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_amm_placement_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmmPlacement.cpp) + target_include_directories(guest_amm_placement_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_amm_placement_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_amm_placement_tests) + add_test(NAME guest_amm_placement COMMAND guest_amm_placement_tests) + if(WIN32) + set_tests_properties(guest_amm_placement PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ampr_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmprThreads.cpp) target_include_directories(guest_ampr_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ampr_threads_tests PRIVATE libSceAmpr libkernel libc) diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 2f02aeebd..b42cd1951 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -19,6 +19,7 @@ bool GuestArenaAvailable_nid_postfix(); bool GuestArenaContains_nid_postfix(const void* pointer, std::size_t bytes); void* GuestArenaAllocate_nid_postfix(std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); +std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment); void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index fea67b8cc..0c0d6f9e9 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -79,6 +79,21 @@ public: return reinterpret_cast(candidate); } + std::uintptr_t HighestFree(std::size_t bytes, std::size_t alignment) { + if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); + if (bytes == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena window"); + std::lock_guard lock(_lock); + auto limit = _end; + while (limit >= _base && bytes <= limit - _base) { + const auto candidate = (limit - bytes) & ~(alignment - 1); + if (candidate < _base) break; + const auto next = _used.lower_bound(candidate + bytes); + if (next == _used.begin() || std::prev(next)->second <= candidate) return candidate; + limit = std::prev(next)->first; + } + return 0; + } + bool MarkUsedForCommit(const void* pointer, std::size_t bytes) { std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); @@ -264,6 +279,10 @@ void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t b return Arena::Get().AllocateAtOrAbove(hint, bytes, alignment); } +std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment) { + return Arena::Get().HighestFree(bytes, alignment); +} + void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes) { Arena::Get().MarkUsed(pointer, bytes); } diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8aa37a332..21005195c 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -248,6 +248,7 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { } constexpr std::uint64_t AmmRangeBytes = 32ull << 30; +constexpr std::uint64_t AmmRangeAlignment = 2ull << 20; constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -279,7 +280,9 @@ std::uintptr_t _ammBase(AmmState& state) { std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); if (arenaBase == 0 || arenaBytes < 2 * AmmRangeBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); - void* address = reinterpret_cast(arenaBase + arenaBytes - 2 * AmmRangeBytes); + const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(2 * AmmRangeBytes, AmmRangeAlignment); + if (top == 0) throw std::runtime_error("AMM: the guest address space arena has no free virtual address range"); + void* address = reinterpret_cast(top); if (DoReserveVirtual(&address, 2 * AmmRangeBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp new file mode 100644 index 000000000..8faaad4c2 --- /dev/null +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t*, std::uint64_t*, std::uint64_t*, std::uint64_t*); +} + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int main() { + try { + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(arenaBase != 0 && arenaBytes != 0, "the guest arena is unavailable"); + const std::uint64_t arenaEnd = arenaBase + arenaBytes; + constexpr std::uint64_t occupied = 4ull << 20; + GuestArena::GuestArenaMarkUsed_nid_postfix(reinterpret_cast(arenaEnd - occupied), occupied); + + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); + Require(end == multimapStart && multimapEnd - start == 64ull << 30, "AMM ranges are not two adjacent 32 GiB ranges"); + Require(start >= arenaBase && multimapEnd <= arenaEnd - occupied, "AMM ranges overlap the used top of the guest arena"); + Require(multimapEnd > arenaEnd - occupied - (1ull << 30), "AMM ranges are not at the top of the free guest arena"); + std::puts("AMM ranges placed below the used top of the guest arena"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 43b223fac..ef885e1c9 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -690,7 +690,7 @@ void TestAmmRangesAboveHintedMaps() { std::uintptr_t arenaBase = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); - Require(multimapEnd == arenaBase + arenaBytes); + Require(start >= arenaBase && multimapEnd <= arenaBase + arenaBytes && heap + 4 * step <= start); for (std::uint64_t hint = heap + step; hint < heap + 4 * step; hint += step) { void* mapped = reinterpret_cast(hint); From a5465007fa95259149bafdc435deca65d87627cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 17:10:33 +0200 Subject: [PATCH 121/277] test(conventions): merge duplicate import --- tools/tests/test_check_conventions_comments.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index a584fcd23..89df82af5 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -8,8 +8,7 @@ import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body -from check_conventions import Check, STUB_BODY, function_body, links, output +from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output class FunctionBodyTests(unittest.TestCase): From 1f34f6e8d44683f4e52639f8159a6b394468409b Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:11:26 +0200 Subject: [PATCH 122/277] fix(relinker): preserve inputs when writing desktop assets --- core/relinker/main.cpp | 22 +++--- .../relinker/tests/test_input_preservation.py | 67 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f1b4a5b64..76afdbb42 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -115,6 +115,19 @@ int Run(const int argc, char* argv[]) { const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { + if (!std::filesystem::exists(path)) return; + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& artifact : guestArtifacts) + if (std::filesystem::equivalent(artifact.SourcePath, path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); + }; + if (desktopEntry) { + checkOutputInputs(desktopEntry->IconPath, "Icon"); + checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); + } + if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); std::vector registryPaths{ @@ -122,14 +135,7 @@ int Run(const int argc, char* argv[]) { }; for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); - for (const auto& path : registryPaths) { - if (!std::filesystem::exists(path)) continue; - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) - throw Domain::RelinkerException("Registry output would overwrite the input executable: " + Io::Utf8Path(path)); - for (const auto& artifact : guestArtifacts) - if (std::filesystem::equivalent(artifact.SourcePath, path)) - throw Domain::RelinkerException("Registry output would overwrite an input module: " + Io::Utf8Path(path)); - } + for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); const auto registryWriter = std::make_shared(); fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); for (std::size_t index = 0; index < guestArtifacts.size(); ++index) diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 851e35fcf..2114abbe3 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -5,6 +5,7 @@ import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_linux_desktop_entry_output import png def check_registry_inputs(relinker, work, windows): @@ -52,10 +53,66 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_desktop_inputs(relinker, work): + for desktop in (False, True): + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"desktop-{desktop}-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + sce_sys = case / "sce_sys" + sce_sys.mkdir() + image = png(1, 1) + (sce_sys / "icon0.png").write_bytes(image) + output = (modules if bundled and alias == "direct" else case) / "output.elf" + paths = [Path(f"{output}.png"), Path(f"{output}.desktop")] + target = paths[int(desktop)] + source = target if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = target if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, target) + elif alias == "symlink": + try: + target.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Desktop symbolic-link case skipped: Windows symlink privilege unavailable") + continue + previous_output = b"previous executable" + output.write_bytes(previous_output) + untouched = paths[int(not desktop)] + previous_auxiliary = b"previous auxiliary output" + untouched.write_bytes(previous_auxiliary) + registry_path = output.with_suffix(".registry.json") + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = ["--registry"] if registry else [] + result = subprocess.run([str(relinker), *options, str(source), str(output)], + capture_output=True, text=True, timeout=20) + kind = "Desktop entry" if desktop else "Icon" + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(target) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_auxiliary + assert registry_path.read_bytes() == previous_registry + assert target.read_bytes() == (module_original if bundled else original) + assert (sce_sys / "icon0.png").read_bytes() == image + assert not (output.parent / "app0").exists() + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_desktop_inputs(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) @@ -115,6 +172,13 @@ def main(): module_original = guest_fixture(PLAIN_SITE) module.write_bytes(module_original) output = case / ("output.exe" if windows else "output.elf") + if not windows: + (case / "sce_sys").mkdir() + image = png(1, 1) + (case / "sce_sys" / "icon0.png").write_bytes(image) + if existing: + Path(f"{output}.png").write_bytes(b"previous icon") + Path(f"{output}.desktop").write_bytes(b"previous desktop entry") if existing: output.write_bytes(b"previous output") result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], @@ -126,6 +190,9 @@ def main(): or not output.with_suffix(".registry.json").exists() or source.read_bytes() != original or module.read_bytes() != module_original): raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr)) + if not windows: + assert Path(f"{output}.png").read_bytes() == image + assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n") print("Input preservation tests passed") diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..ca2718f95 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ 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`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, 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 8b73fe7a76c1a624ca48908089a62b8b3ce40f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 17:11:35 +0200 Subject: [PATCH 123/277] fix(stubgen): give new output files the default umask permissions --- tools/stubgen.py | 5 +++++ tools/tests/test_stubgen.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tools/stubgen.py b/tools/stubgen.py index d3c752b53..e12520f43 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import os import re import sys import tempfile @@ -177,6 +178,8 @@ def same_file(left, right): def write_outputs(outputs): staged = [] + umask = os.umask(0) + os.umask(umask) try: for filename, content in outputs: path = Path(filename).resolve() @@ -188,6 +191,8 @@ def write_outputs(outputs): handle.write(content) if path.exists(): temporary.chmod(path.stat().st_mode & 0o7777) + else: + temporary.chmod(0o666 & ~umask) for temporary, path in staged: temporary.replace(path) finally: diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index f8714e203..ae8d0d6bf 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,5 +1,6 @@ import contextlib import io +import os import tempfile from pathlib import Path import sys @@ -113,6 +114,10 @@ class StubGeneratorTests(unittest.TestCase): self.assertEqual(stubgen.main(arguments), 0) self.assertIn('APS5_EXPORT("AAAAAAAAAAA",', code.read_text(encoding="utf-8")) self.assertIn("unknown name, signature", debt.read_text(encoding="utf-8")) + if sys.platform != "win32": + mask = os.umask(0o022) + os.umask(mask) + self.assertEqual(code.stat().st_mode & 0o777, 0o666 & ~mask) if __name__ == "__main__": From 4b91755d9a4081d5cb0223261a379d859998ef5f Mon Sep 17 00:00:00 2001 From: virp-mta Date: Sun, 11 Oct 2026 19:01:00 +0400 Subject: [PATCH 124/277] feat(libSceSigninDialog): complete offline sign-in dialog lifecycle --- core/libs/CMakeLists.txt | 10 +++ core/libs/prx/libSceSigninDialog/Export.cpp | 22 ++++-- core/libs/tests/GuestSigninDialog.cpp | 77 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestSigninDialog.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..f02604ae4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -188,6 +188,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp) + target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc) + configure_windows_unwind(guest_signin_dialog_tests) + add_test(NAME guest_signin_dialog COMMAND guest_signin_dialog_tests) + if(WIN32) + set_tests_properties(guest_signin_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp) target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc) diff --git a/core/libs/prx/libSceSigninDialog/Export.cpp b/core/libs/prx/libSceSigninDialog/Export.cpp index ea21b63b2..761bf3330 100644 --- a/core/libs/prx/libSceSigninDialog/Export.cpp +++ b/core/libs/prx/libSceSigninDialog/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -12,8 +13,15 @@ constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +struct SceSigninDialogResult { + std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; + std::int32_t reserved[3] = {}; +}; std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -22,19 +30,23 @@ std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; extern "C" { int APS5_VABI sceSigninDialogClose(void) { - NotImplemented_nid_no_patch(__func__); + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceSigninDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const SceSigninDialogResult dialogResult{}; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); return 0; } int APS5_VABI sceSigninDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); } int APS5_VABI sceSigninDialogInitialize(void) { diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp new file mode 100644 index 000000000..d8410fcbd --- /dev/null +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceSigninDialogInitialize(void); +int APS5_VABI sceSigninDialogOpen(const void* param); +int APS5_VABI sceSigninDialogUpdateStatus(void); +int APS5_VABI sceSigninDialogGetStatus(void); +int APS5_VABI sceSigninDialogGetResult(void* result); +int APS5_VABI sceSigninDialogClose(void); +int APS5_VABI sceSigninDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[4]; + std::memset(result, 0x5a, sizeof(result)); + + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogInitialize() == kErrAlreadyInitialized); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + + Require(sceSigninDialogOpen(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == kErrNotFinished); + + Require(sceSigninDialogOpen(¶m) == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + Require(sceSigninDialogUpdateStatus() == kStatusFinished); + + Require(sceSigninDialogGetResult(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogTerminate() == 0); + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + std::memset(result, 0xa5, sizeof(result)); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + Require(sceSigninDialogTerminate() == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..4e22d6d95 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -79,6 +79,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp). Name from the shadPS4 aerolib NID list - [sceAppContentDownloadDataGetAvailableSpaceKb](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB - [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - names and the no-argument signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); imported by PPSA23566, not confirmed on a PS5 title +- [sceSigninDialogClose, sceSigninDialogGetResult](../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes follow libSceLoginDialog's 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware - [libSceBluetoothHid](../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - signatures taken from PPSA01317: `sceBluetoothHidInit()` and `sceBluetoothHidUnregisterCallback()` without arguments, `sceBluetoothHidRegisterCallback(callback, 0, param)` with a 16-byte param, and `sceBluetoothHidRegisterDevice` / `sceBluetoothHidUnregisterDevice(vendorId, productId)`; the report and device name exports are declared without parameters - [sceAgcWaitRegMemPatchMask](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters From dc022d8418fbdbfe90535a310979f51292b37392 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 10:27:19 +0400 Subject: [PATCH 125/277] fix(libkernel): let a guest unlink and rename files it has open on Windows FreeBSD removes or renames a file that is still open; the descriptor stays usable. _wopen opens without FILE_SHARE_DELETE, so sceKernelUnlink returned EACCES and rename failed while the guest held a descriptor. sceKernelOpen now opens through CreateFileW with delete sharing and keeps the flag, mode and error mapping of _wopen. (cherry picked from commit 2f5ada5c86a427505804f22def209e054b3571e4) --- core/libs/prx/libkernel/File/src/Open.cpp | 49 ++++++++++++++++++++++- core/libs/tests/GuestFilesystem.cpp | 15 +++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ca7cbe5f1..02b338d7d 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -20,8 +20,55 @@ #include #include #include +#include +static int OpenError(DWORD error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRITE_PROTECT: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_DRIVE_LOCKED: + return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + return EEXIST; + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ENOMEM; + case ERROR_DISK_FULL: + return ENOSPC; + default: + return EINVAL; + } +} static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { - return ::_wopen(p.wstring().c_str(), nativeFlags, static_cast(mode)); + const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); + const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; + DWORD disposition = OPEN_EXISTING; + if (nativeFlags & _O_CREAT) disposition = (nativeFlags & _O_EXCL) ? CREATE_NEW : (nativeFlags & _O_TRUNC) ? CREATE_ALWAYS : OPEN_ALWAYS; + else if (nativeFlags & _O_TRUNC) disposition = TRUNCATE_EXISTING; + const DWORD attributes = (nativeFlags & _O_CREAT) && !(mode & _S_IWRITE) ? FILE_ATTRIBUTE_READONLY : FILE_ATTRIBUTE_NORMAL; + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = OpenError(GetLastError()); + return -1; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY)); + if (descriptor < 0) CloseHandle(handle); + return descriptor; } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { return ::_lseeki64(fd, offset, whence); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..d7b02ceef 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -136,6 +136,21 @@ 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"; } + const auto held = root / "held.txt"; + const auto heldName = held.string(); + { std::ofstream stream(held); stream << "held"; } + const int heldDescriptor = sceKernelOpen(heldName.c_str(), 2, 0); + Require(heldDescriptor >= 0 && sceKernelUnlink(heldName.c_str()) == 0 && !std::filesystem::exists(held)); + char heldBytes[4]{}; + Require(read_nid_postfix(heldDescriptor, heldBytes, sizeof(heldBytes)) == sizeof(heldBytes) && std::memcmp(heldBytes, "held", 4) == 0); + const int recreated = sceKernelOpen(heldName.c_str(), 0x0a01, 0644); + Require(recreated >= 0 && sceKernelClose(heldDescriptor) == 0 && std::filesystem::file_size(held) == 0); + const auto moved = root / "moved.txt"; + Require(rename_nid_postfix(heldName.c_str(), moved.string().c_str()) == 0 && !std::filesystem::exists(held)); + Require(write_nid_postfix(recreated, "moved", 5) == 5 && sceKernelClose(recreated) == 0 && std::filesystem::file_size(moved) == 5); + const int removing = open_nid_postfix(moved.string().c_str(), 0, 0); + Require(removing >= 0 && remove_nid_postfix(moved.string().c_str()) == 0 && !std::filesystem::exists(moved)); + Require(close_nid_postfix(removing) == 0); Require(remove_nid_postfix(root.string().c_str()) == -1); Require(*__error_nid_postfix() == 66); Require(std::filesystem::is_regular_file(file)); From b57627322534a5041d3893797f641278b6f9d55a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:48:37 +0200 Subject: [PATCH 126/277] fix(libSceFont): return the letter step of sceFontWritingRefersRenderStepCharacter through a pointer The second argument is an out pointer to the writing's letter step, as KytyPS5 and the SharpProspero bindings declare it. It was filled as a letter step struct, which wrote 64 bytes over the caller's 8-byte pointer. The letter step now lives in the writing and the call stores its address, or null when there is no current step. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceFont/src/Text.cpp | 30 ++++++++-------- core/libs/tests/GuestFont.cpp | 52 +++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 5a43ff63f..1c81e6977 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -63,10 +63,11 @@ struct FontWritingData { bool hasCurrentStep = false; bool maskFormatCharacters = true; bool ownsStorage = false; + bool hasRawGlyphMetrics = false; FontWritingStep step{}; FontWritingMetrics metrics{}; FontGlyphMetrics rawGlyphMetrics{}; - bool hasRawGlyphMetrics = false; + FontWritingLetterStep letterStep{}; }; static_assert(sizeof(FontWritingData) <= sizeof(FontWriting)); @@ -596,21 +597,22 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon return &writing->step; } -FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, FontWritingLetterStep* letterStep) { - if (letterStep) std::memset(letterStep, 0, sizeof(*letterStep)); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep) { + if (pLetterStep) *pLetterStep = nullptr; auto* writing = GetWritingData(fontWriting); if (!writing || !writing->hasCurrentStep || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; - if (letterStep) { - letterStep->x = writing->step.x; - letterStep->y = writing->step.y; - letterStep->advanceX = writing->step.advanceX; - letterStep->advanceY = writing->step.advanceY; - letterStep->Components.textsCount = 1; - letterStep->Components.textsIndex = static_cast(writing->currentIndex); - letterStep->Components.glyphsCount = 1; - letterStep->Components.glyphsIndex = static_cast(writing->currentIndex); - letterStep->Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); - } + auto& letterStep = writing->letterStep; + letterStep = {}; + letterStep.x = writing->step.x; + letterStep.y = writing->step.y; + letterStep.advanceX = writing->step.advanceX; + letterStep.advanceY = writing->step.advanceY; + letterStep.Components.textsCount = 1; + letterStep.Components.textsIndex = static_cast(writing->currentIndex); + letterStep.Components.glyphsCount = 1; + letterStep.Components.glyphsIndex = static_cast(writing->currentIndex); + letterStep.Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); + if (pLetterStep) *pLetterStep = &letterStep; return &writing->storage->characters[writing->currentIndex]; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index f87745e78..82bcd6951 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -45,6 +45,13 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_t); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); +int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); +int APS5_VABI sceFontCreateString(const FontMemory*, FontTextSource*, const FontCreateStringDetail*, FontString*); +int APS5_VABI sceFontDestroyString(FontString*); +FontTextCharacter* APS5_VABI sceFontStringRefersTextCharacters(FontString, std::uint32_t*); +int APS5_VABI sceFontWritingInit(FontWriting*, FontString, const FontTextCharacter*); +const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting*); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting*, const FontWritingLetterStep**); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); } @@ -126,6 +133,24 @@ static std::vector KerningFont() { }); } +struct ParsedText { + const char* text; + std::size_t at; + FontHandle font; +}; + +static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast(source->textObject); + *order = const_cast(parsed->text + parsed->at); + if (parsed->text[parsed->at] == 0) { + result->Terminate.terminateCode = 0; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = static_cast(parsed->text[parsed->at++]); + return 1; +} + static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } @@ -307,6 +332,33 @@ int main() { Require(sceFontGetRenderScaledKerning(&unopened, 'A', 'V', &kerning) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && KerningIs(kerning, 0.0f)); Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + + ParsedText parsed{"AV", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t characterCount = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &characterCount); + Require(characters != nullptr && characterCount == 2); + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + constexpr std::uint64_t guard = 0x5a5a5a5a5a5a5a5aull; + struct { + const FontWritingLetterStep* letter; + std::uint64_t guard; + } out{reinterpret_cast(&memory), guard}; + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr && out.guard == guard); + const FontWritingStep* step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters && out.letter != nullptr && out.guard == guard); + Require(out.letter->x == step->x && out.letter->advanceX == step->advanceX && out.letter->Components.textsIndex == 0 && out.letter->Components.characterTextCount == 1); + step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.guard == guard); + Require(out.letter->x == step->x && out.letter->Components.textsIndex == 1 && out.letter->Components.glyphsIndex == 1); + Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters + 1); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK && renderer == nullptr); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..1d59fca20 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -123,6 +123,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` - [sceFontGetFontResolution](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title; the outputs are inferred from the names. The resolution is the face's units per EM and the scale pixel the driver's face scale (units per EM / 64 for FreeType), the pixel scale a font has when opened. Either output may be null, as in `sceFontGetResolutionDpi` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title. The 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 +- [sceFontWritingRefersRenderStepCharacter](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures From 674488045021d1c13efd41988eeeb291b5537849 Mon Sep 17 00:00:00 2001 From: Lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:20:39 +0200 Subject: [PATCH 127/277] fix(libc): parse atoi through guest-width strtol --- core/libs/prx/libc/CMakeLists.txt | 10 ++++++ core/libs/prx/libc/src/Strings.cpp | 2 +- core/libs/tests/GuestAtoi.cpp | 55 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestAtoi.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ddfe85664..3617cba86 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -302,6 +302,16 @@ if(BUILD_TESTING) set_tests_properties(guest_interfaces PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_atoi_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtoi.cpp) + target_include_directories(guest_atoi_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_atoi_tests PRIVATE libc) + configure_windows_unwind(guest_atoi_tests) + add_test(NAME guest_atoi COMMAND guest_atoi_tests) + set_tests_properties(guest_atoi PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_atoi PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_reallocalign_tests ${LIBS_INCLUDE_DIR}/tests/GuestReallocalign.cpp) target_include_directories(guest_reallocalign_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_reallocalign_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 978a499aa..039d31762 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -176,7 +176,7 @@ long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { } int APS5_VABI atoi_nid_postfix(const char* str) { - return std::atoi(str); + return static_cast(strtol_nid_postfix(str, nullptr, 10)); } std::int64_t APS5_VABI atol_nid_postfix(const char* str) { diff --git a/core/libs/tests/GuestAtoi.cpp b/core/libs/tests/GuestAtoi.cpp new file mode 100644 index 000000000..0c4d77ddf --- /dev/null +++ b/core/libs/tests/GuestAtoi.cpp @@ -0,0 +1,55 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI atoi_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +int main() { + struct Case { + const char* text; + int value; + int error; + }; + constexpr int SavedError = 1234; + constexpr Case cases[] = { + {"0", 0, SavedError}, + {"+42", 42, SavedError}, + {" \t\n\r\f\v-0012tail", -12, SavedError}, + {"010", 10, SavedError}, + {"0x10", 0, SavedError}, + {"0b10", 0, SavedError}, + {"", 0, SavedError}, + {"words", 0, SavedError}, + {" +", 0, SavedError}, + {"2147483647", INT32_MAX, SavedError}, + {"-2147483648", INT32_MIN, SavedError}, + {"2147483648", INT32_MIN, SavedError}, + {"-2147483649", INT32_MAX, SavedError}, + {"4294967295", -1, SavedError}, + {"-4294967295", 1, SavedError}, + {"4294967296", 0, SavedError}, + {"-4294967296", 0, SavedError}, + {" \t+4294967297tail", 1, SavedError}, + {"9223372036854775807", -1, SavedError}, + {"-9223372036854775808", 0, SavedError}, + {"9223372036854775808", -1, 34}, + {"-9223372036854775809", 0, 34}, + {"999999999999999999999999999999999999", -1, 34}, + {"-999999999999999999999999999999999999", 0, 34}, + }; + bool correct = true; + for (const auto& test : cases) { + *__error_nid_postfix() = SavedError; + const int value = atoi_nid_postfix(test.text); + const int error = *__error_nid_postfix(); + if (value != test.value || error != test.error) { + std::fprintf(stderr, "atoi(%s): expected %d, errno %d; received %d, errno %d\n", + test.text, test.value, test.error, value, error); + correct = false; + } + } + return correct ? 0 : 1; +} From 1a6f6a9f13bcc7d96b5d3b640440c8f73c05d298 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:22:51 -0400 Subject: [PATCH 128/277] fix(libc): match FreeBSD asinf errno asinf returned 0xffc00000 (errno 33) for 2.0 (0x40000000), where FreeBSD lib/msun/src/math.h returns 0xffc00000 (errno 0). It now returns the FreeBSD result for that case. A guest test covers it and fails on main. Co-Authored-By: Claude Haiku 5.5 --- core/libs/prx/libc/src/Math.cpp | 7 ++++++- core/libs/tests/GuestMath.cpp | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..b11f34f1b 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -36,7 +36,12 @@ std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t numerator, std::int64_t den } float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } -float APS5_VABI asinf_nid_postfix(float x) { return std::asin(x); } +float APS5_VABI asinf_nid_postfix(float x) { + const int saved_errno = errno; + const float result = std::asin(x); + errno = saved_errno; + return result; +} float APS5_VABI acosf_nid_postfix(float x) { return std::acos(x); } float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..ec964603b 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -128,6 +128,12 @@ static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), -INFINITY)) == 0x7ff0000000000000ull); } +static void CheckAsinfErrno() { + errno = 0; + (void)asinf_nid_postfix(2.0f); + Require(errno == 0); +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -479,6 +485,7 @@ int main() { Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); Require(__isnormal_nid_postfix(std::numeric_limits::denorm_min()) == 0); const auto quotient = div_nid_postfix(7, 2); + CheckAsinfErrno(); Require(quotient.quot == 3 && quotient.rem == 1); const auto negativeNumerator = div_nid_postfix(-7, 2); Require(negativeNumerator.quot == -3 && negativeNumerator.rem == -1); From 3ae258380253111216baaf236f4bbe2e11a1f0cf Mon Sep 17 00:00:00 2001 From: exefer Date: Sun, 11 Oct 2026 14:00:51 +0200 Subject: [PATCH 129/277] fix(libkernel): select descriptors that are not sockets --- core/libs/prx/libkernel/CMakeLists.txt | 8 + core/libs/prx/libkernel/Socket/src/Socket.cpp | 147 ++++++++++++++++-- core/libs/tests/GuestSelect.cpp | 119 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 core/libs/tests/GuestSelect.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..69a9f48a4 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -797,6 +797,14 @@ if(BUILD_TESTING) set_tests_properties(guest_poll 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) + endif() add_test_executable(guest_tcp_tests ${LIBS_INCLUDE_DIR}/tests/GuestTcp.cpp) target_include_directories(guest_tcp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_tcp_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index f7e393e53..3f71f4f1c 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -5,18 +5,25 @@ #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include +#include #include #include #include #include +#ifndef _WIN32 +#include +#endif + namespace { constexpr int GuestEinval = 22; constexpr int SelectDescriptorLimit = 1024; +constexpr int SelectPollSliceMilliseconds = 10; struct GuestTimeval { std::int64_t seconds; @@ -38,6 +45,45 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +#ifndef _WIN32 + +short HostEventFlags(short events) { + short flags = 0; + if ((events & KernelSocketPoll::Readable) != 0) flags |= POLLIN; + if ((events & KernelSocketPoll::Writable) != 0) flags |= POLLOUT; + return flags; +} + +short KernelEventFlags(short revents) { + short flags = 0; + if ((revents & POLLIN) != 0) flags |= KernelSocketPoll::Readable; + if ((revents & POLLOUT) != 0) flags |= KernelSocketPoll::Writable; + if ((revents & POLLERR) != 0) flags |= KernelSocketPoll::Error; + if ((revents & POLLHUP) != 0) flags |= KernelSocketPoll::HangUp; + return flags; +} + +int HostPoll(KernelSocketPoll::Entry* descriptors, std::size_t count, int timeoutMilliseconds) { + std::vector native(count); + for (std::size_t index = 0; index < count; ++index) { + native[index].fd = descriptors[index].descriptor; + native[index].events = descriptors[index].events; + native[index].revents = 0; + } + const int result = ::poll(native.data(), static_cast(count), timeoutMilliseconds); + if (result < 0) return -1; + for (std::size_t index = 0; index < count; ++index) { + if ((native[index].revents & POLLNVAL) != 0) { + errno = EBADF; + return -1; + } + descriptors[index].revents = KernelEventFlags(native[index].revents); + } + return result; +} + +#endif + } extern "C" { @@ -65,8 +111,12 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* *__error_nid_postfix() = GuestEinval; return -1; } - std::vector entries; + std::vector sockets; + std::vector socketSlots; + std::vector hosts; + std::vector hostSlots; std::vector devices; + std::vector requested; for (int descriptor = 0; descriptor < nfds; ++descriptor) { short events = 0; if (DescriptorSet(readfds, descriptor)) events |= KernelSocketPoll::Readable; @@ -75,11 +125,56 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if (events == 0) continue; if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); - else entries.push_back({descriptor, events, 0}); + else { + sockets.push_back({descriptor, events, 0}); + socketSlots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); + } } - const auto sockets = entries.size(); + for (const auto& device : devices) requested.push_back(device); const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); - entries.insert(entries.end(), devices.begin(), devices.end()); + const auto poller = g_socketPoller.load(); + if (!sockets.empty()) { + if (poller == nullptr) { + for (std::size_t index = 0; index < sockets.size();) { + const int descriptor = sockets[index].descriptor; +#ifndef _WIN32 + if (descriptor < GuestSockets::FirstDescriptor) { + hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); + hostSlots.push_back(socketSlots[index]); + sockets.erase(sockets.begin() + static_cast(index)); + socketSlots.erase(socketSlots.begin() + static_cast(index)); + continue; + } +#endif + UnknownDescriptor(descriptor, false); + } + } else { + for (auto& socket : sockets) socket.revents = 0; + const int probe = poller(sockets.data(), static_cast(sockets.size()), 0); + if (probe < 0) { + *__error_nid_postfix() = -probe; + return -1; + } + for (std::size_t index = 0; index < sockets.size();) { + if ((sockets[index].revents & KernelSocketPoll::Unknown) == 0) { + ++index; + continue; + } + const int descriptor = sockets[index].descriptor; +#ifndef _WIN32 + if (descriptor < GuestSockets::FirstDescriptor) { + hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); + hostSlots.push_back(socketSlots[index]); + sockets.erase(sockets.begin() + static_cast(index)); + socketSlots.erase(socketSlots.begin() + static_cast(index)); + continue; + } +#endif + UnknownDescriptor(descriptor, true); + } + } + } const auto start = std::chrono::steady_clock::now(); const auto deadline = limit == nullptr ? std::chrono::steady_clock::time_point::max() : KernelSocketSelect::DeadlineAfter(start, limit->seconds, limit->microseconds); @@ -91,20 +186,48 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; - if (sockets != 0) { - const auto poller = g_socketPoller.load(); - if (poller == nullptr) UnknownDescriptor(entries.front().descriptor, false); - for (std::size_t index = 0; index < sockets; ++index) entries[index].revents = 0; - const int result = poller(entries.data(), static_cast(sockets), waitMilliseconds); + if (!hosts.empty() && !sockets.empty()) { + if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); + const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); + for (;;) { + for (auto& host : hosts) host.revents = 0; + const int hostReady = HostPoll(hosts.data(), hosts.size(), SelectPollSliceMilliseconds); + if (hostReady < 0) { + *__error_nid_postfix() = errno; + return -1; + } + for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; + for (auto& socket : sockets) socket.revents = 0; + const int socketReady = poller(sockets.data(), static_cast(sockets.size()), SelectPollSliceMilliseconds); + if (socketReady < 0) { + *__error_nid_postfix() = -socketReady; + return -1; + } + for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; + if (hostReady > 0 || socketReady > 0) break; + if (std::chrono::steady_clock::now() >= sliceEnd) break; + } + } else if (!hosts.empty()) { + for (auto& host : hosts) host.revents = 0; + if (HostPoll(hosts.data(), hosts.size(), waitMilliseconds) < 0) { + *__error_nid_postfix() = errno; + return -1; + } + for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; + } else if (!sockets.empty()) { + if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); + for (auto& socket : sockets) socket.revents = 0; + const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); if (result < 0) { *__error_nid_postfix() = -result; return -1; } + for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; } else if (waitMilliseconds > 0) { std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); } int ready = 0; - for (const auto& entry : entries) { + for (const auto& entry : requested) { if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor, true); 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; @@ -112,7 +235,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* ready += static_cast(readable) + static_cast(writable) + static_cast(urgent); } if (ready > 0) { - for (const auto& entry : entries) { + for (const auto& entry : requested) { if ((entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) == 0) ClearDescriptor(readfds, entry.descriptor); if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); @@ -120,7 +243,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return ready; } if (limit != nullptr && std::chrono::steady_clock::now() >= deadline) { - for (const auto& entry : entries) { + for (const auto& entry : requested) { ClearDescriptor(readfds, entry.descriptor); ClearDescriptor(writefds, entry.descriptor); ClearDescriptor(exceptfds, entry.descriptor); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp new file mode 100644 index 000000000..bd6cd8d50 --- /dev/null +++ b/core/libs/tests/GuestSelect.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct GuestDescriptorSet { + std::array words{}; + + void Add(int descriptor) { + words[descriptor / 64] |= (std::uint64_t{1} << (descriptor % 64)); + } + + bool Has(int descriptor) const { + return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; + } +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; +constexpr int GuestDescriptorLimit = 1024; + +int main() { + int descriptors[2]{-1, -1}; + Require(pipe_nid_postfix(descriptors) == 0); + const int reader = descriptors[0]; + const int writer = descriptors[1]; + Require(reader >= 0 && writer >= 0 && reader != writer); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 200000}; + const auto start = std::chrono::steady_clock::now(); + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(!read.Has(reader)); + } + + { + const char byte = 's'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestDescriptorSet except{}; + read.Add(reader); + except.Add(reader); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, except.words.data(), &limit) == 1); + Require(read.Has(reader)); + Require(!except.Has(reader)); + char buffer[1]{}; + Require(read_nid_postfix(reader, buffer, sizeof(buffer)) == 1); + Require(buffer[0] == byte); + } + + { + GuestDescriptorSet write{}; + write.Add(writer); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(writer + 1, nullptr, write.words.data(), nullptr, &limit) == 1); + Require(write.Has(writer)); + } + + { + const char byte = 'e'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + read.Add(reader); + write.Add(writer); + const int highest = reader > writer ? reader : writer; + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), nullptr, &limit) == 2); + Require(read.Has(reader) && write.Has(writer)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 100000}; + Require(select_nid_postfix(reader, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(read.Has(reader)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(GuestDescriptorLimit + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEinval); + } + + Require(close_nid_postfix(reader) == 0); + Require(close_nid_postfix(writer) == 0); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEbadf); + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..322038833 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -195,6 +195,7 @@ Throughout the project, every function at every stage either **does exactly what - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified - [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. Other descriptors that are not libSceNet sockets throw. 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 - [__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](../../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` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. The ids and privileges a title has on a console are unverified From 2f6db82da7f4fb25e2cc7bbb872665ce7b1c812d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:30:28 +0400 Subject: [PATCH 130/277] fix(nid-names): normalize Windows inventory paths --- tools/nid_names.py | 2 +- tools/tests/test_nid_names.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index edb87dc69..8dd9382e7 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -97,7 +97,7 @@ def collect_unknowns(): for path in source_files(PRX): text = path.read_text(encoding="utf-8") for nid, name in source_aliases(text): - found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT), name)) + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) return found diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 39711f949..0ac509da5 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -4,7 +4,7 @@ import json import tempfile import sys import unittest -from pathlib import Path +from pathlib import Path, PureWindowsPath from unittest.mock import patch from urllib.error import URLError @@ -161,6 +161,12 @@ int APS5_VABI sceReal() { return 0; } self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_windows_source_paths_use_portable_separators(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n') + relative = PureWindowsPath("core/libs/prx/libSceExample/Export.cpp") + with patch.object(Path, "relative_to", return_value=relative): + self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_test_fixtures_are_not_runtime_implementations(self): self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nint APS5_VABI sceFixture() { return 0; }\n', "tests/Fixture.cpp") self.assertEqual(nid_names.collect_unknowns(), {}) From e6dcb46f3ae4c2811aed4fb9ff277413967d8f9c Mon Sep 17 00:00:00 2001 From: virp-mta Date: Sun, 11 Oct 2026 00:52:46 +0400 Subject: [PATCH 131/277] feat(libSceNpEntitlementAccess): implement offline entitlement info requests --- core/libs/prx/libSceNpEntitlementAccess/Export.cpp | 12 ++++-------- core/libs/tests/GuestOfflineImports.cpp | 8 ++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index d0bc1f710..90aeb0b06 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -135,23 +135,19 @@ int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 853a4bf1c..6f064ed62 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -17,6 +17,10 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpActivate(); int APS5_VABI sceRudpGetStatus(void*, std::size_t); @@ -67,6 +71,10 @@ int main() { Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessPollServiceEntitlementInfoList() == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); Require(sceRudpActivate() == 0); std::array status; From 19344d661a357a8f2a3e337dae1f7d8e7464a094 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:30:59 +0400 Subject: [PATCH 132/277] test(packaging): restore cwd before temporary cleanup --- tools/tests/test_package_release.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index c00c6b237..2724ef6e4 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -165,10 +165,7 @@ class ReleaseAssetsTests(unittest.TestCase): names = ["prx-linux-v1.zip", "prx-linux-v1.tar.gz", "relinker-v1"] for name in names: (output / name).write_bytes(b"old " + name.encode()) - previous = Path.cwd() - os.chdir(source) - self.addCleanup(os.chdir, previous) - with patch.object(package_release, "ROOT", source), \ + with working_directory(source), patch.object(package_release, "ROOT", source), \ patch.object(package_release.tarfile, "open", side_effect=OSError("disk full")), \ self.assertRaisesRegex(OSError, "disk full"): package_release.package("linux", build, output, "v1") From e127a525d15db01d081f941edf74dca68305b0e3 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:34:20 +0200 Subject: [PATCH 133/277] feat(libSceFont): implement sceFontCharacterGetSyllableStringState The call was an unimplemented stub. It now writes syllable state 0 and returns OK for every character outside the Thai block, where a PS4 firmware 11.00 libSceFont measures 1, 3 or 4; Thai characters throw as not implemented. A null character or result returns SCE_FONT_ERROR_INVALID_PARAMETER, and a null character writes 0 to a non-null result, as measured. The signature follows SharpProspero. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceFont/src/Text.cpp | 11 +++++++++++ core/libs/prx/libSceFont/src/Unimplemented.cpp | 5 ----- core/libs/tests/GuestFont.cpp | 8 ++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 5a43ff63f..791c64491 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "prx/libSceFont/include/FontInternal.hpp" @@ -491,6 +492,16 @@ int APS5_VABI sceFontCharacterGetBidiLevel(const FontTextCharacter* textCharacte return SCE_FONT_OK; } +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter* textCharacter, int* syllableStringState) { + if (!textCharacter || !syllableStringState) { + if (syllableStringState) *syllableStringState = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (textCharacter->characterCode >= 0x0E00 && textCharacter->characterCode <= 0x0E7F) throw std::runtime_error("libSceFont: sceFontCharacterGetSyllableStringState for Thai syllable clusters is not implemented"); + *syllableStringState = 0; + return SCE_FONT_OK; +} + int APS5_VABI sceFontCharacterGetTextFontCode(const FontTextCharacter* textCharacter, FontHandle* pFontHandle, std::uint32_t* textCode) { if (!textCharacter) { if (pFontHandle) *pFontHandle = nullptr; diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 53b3e2ec9..b3c2e79b7 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -7,11 +7,6 @@ extern "C" { -int APS5_VABI sceFontCharacterGetSyllableStringState() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontCharactersRefersTextCodes() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index f87745e78..9a61f20a3 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -47,6 +47,7 @@ int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_ int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter*, int*); } static void Check(bool value, int line) { @@ -317,5 +318,12 @@ int main() { Require(allocations == 0); Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); Require(sceFontMemoryTerm(&memory) == SCE_FONT_ERROR_INVALID_MEMORY); + FontTextCharacter character{}; + character.characterCode = 'A'; + int syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(&character, &syllableState) == SCE_FONT_OK && syllableState == 0); + syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(nullptr, &syllableState) == SCE_FONT_ERROR_INVALID_PARAMETER && syllableState == 0); + Require(sceFontCharacterGetSyllableStringState(&character, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); std::filesystem::remove_all(fontRoot); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..99de3967c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -155,6 +155,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown +- [sceFontCharacterGetSyllableStringState](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function From 86da3e9d7428aaaef00dd00e9a0934930ae9fa82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 17:34:21 +0200 Subject: [PATCH 134/277] fix(libc): include cerrno in Math.cpp --- core/libs/prx/libc/src/Math.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index b11f34f1b..37a3bec8c 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include From 6293e6ae6310ee8971de87f34dc5818330600abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 17:34:43 +0200 Subject: [PATCH 135/277] docs(technical-debt): cover the single-item NpEntitlementAccess info requests --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..03120c0fc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -66,7 +66,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioPropagationSourceSetAudioPath](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. - [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters and bilinear cube comparisons without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfoList](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified +- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified - [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceHttpSetCookieEnabled](../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified From 99ddfa762acf96bd3e140dbd80c21f6e8d174667 Mon Sep 17 00:00:00 2001 From: Utku Halis Date: Sun, 11 Oct 2026 07:31:17 +0300 Subject: [PATCH 136/277] fix(libc): track never-collected pages in the macOS write watch On macOS, GuestWriteWatchCollectFresh reported nothing and returned true, so MarkWritten took pages no collection had armed yet for clean, and they stayed writable without being tracked. GuestWriteWatchCollectArmed was a full collect, which also took those pages. Pages now carry a fresh bit from registration until a collection takes them, as the Linux watch keeps its fresh ranges: CollectFresh reports and arms only fresh pages, CollectArmed skips them, and Collect takes both. Protection changes, host writes and faults keep the bit. The write_watch fresh and armed tests check both collections. --- core/libs/prx/libc/src/GuestWriteWatch.cpp | 29 ++++++++++++-------- core/libs/tests/WriteWatch.cpp | 32 ++++++++++++++++++++-- core/libs/tests/write_watch/CMakeLists.txt | 2 +- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 109286ce8..55f991f04 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -333,7 +333,7 @@ public: begin = pageStart(begin); end = pageEnd(end); if (!_available || end <= begin) return; - std::vector pages((end - begin) / _page, Written); + std::vector pages((end - begin) / _page, Written | Fresh); std::vector queried(pages.size(), false); const Lock lock(_lock); for (auto cursor = begin; cursor < end;) { @@ -347,7 +347,7 @@ public: const auto last = std::min(end, address + size); const std::uint8_t protection = ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0); for (auto page = first; page < last; page += _page) { - pages[(page - begin) / _page] = Written | protection; + pages[(page - begin) / _page] = Written | Fresh | protection; queried[(page - begin) / _page] = true; } cursor = last; @@ -356,7 +356,7 @@ public: auto& entry = pages[(page - begin) / _page]; const std::uint8_t recorded = state & ProtectionMask; const bool armed = (state & Protected) != 0 && (entry & ProtectionMask) == (recorded & ~PROT_WRITE); - if (armed || !queried[(page - begin) / _page]) entry = static_cast(Written | recorded); + if (armed || !queried[(page - begin) / _page]) entry = static_cast(Written | Fresh | recorded); else state = static_cast(state & ~Protected); }); erase(begin, end); @@ -377,7 +377,9 @@ public: return covers(begin, end); } - bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) { + enum class Take { All, Armed, Fresh }; + + bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context, Take take = Take::All) { if (!_available) return false; begin = pageStart(begin); end = pageEnd(end); @@ -385,9 +387,12 @@ public: std::vector> runs; { const Lock lock(_lock); - if (!covers(begin, end)) return false; + if (take != Take::Fresh && !covers(begin, end)) return false; forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { if ((state & Written) == 0) return; + const bool fresh = (state & Fresh) != 0; + if ((take == Take::Armed && fresh) || (take == Take::Fresh && !fresh)) return; + state &= static_cast(~Fresh); if (!runs.empty() && runs.back().second == page) runs.back().second = page + _page; else runs.emplace_back(page, page + _page); for (const auto& [first, last] : _hostWrites) @@ -407,7 +412,7 @@ public: if (!_available || end <= begin) return; const Lock lock(_lock); forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) { - state = static_cast(Written | (protection & ProtectionMask)); + state = static_cast(Written | (state & Fresh) | (protection & ProtectionMask)); }); } @@ -419,7 +424,7 @@ public: _hostWrites.emplace_back(begin, end); forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { if ((state & Protected) != 0) ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); - state = static_cast((state & ProtectionMask) | Written); + state = static_cast((state & (ProtectionMask | Fresh)) | Written); }); } @@ -453,6 +458,7 @@ private: static constexpr std::uint8_t ProtectionMask = PROT_READ | PROT_WRITE | PROT_EXEC; static constexpr std::uint8_t Written = 0x10; static constexpr std::uint8_t Protected = 0x20; + static constexpr std::uint8_t Fresh = 0x40; struct Range { std::uintptr_t end; @@ -512,7 +518,7 @@ private: if (page >= it->second.end) return false; auto& state = it->second.pages[(page - it->first) / _page]; if ((state & Protected) != 0) { - state = static_cast((state & ProtectionMask) | Written); + state = static_cast((state & (ProtectionMask | Fresh)) | Written); ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); return true; } @@ -708,9 +714,8 @@ bool GuestWriteWatchCollectFresh_nid_postfix(std::uintptr_t address, std::size_t if (bytes == 0 || address + bytes < address) return false; return Watch::Get().CollectFresh(address, address + bytes, written, context); #elif defined(__APPLE__) - static_cast(written); - static_cast(context); - return bytes != 0 && address + bytes >= address; + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Fresh); #else static_cast(address); static_cast(bytes); @@ -726,7 +731,7 @@ bool GuestWriteWatchCollectArmed_nid_postfix(std::uintptr_t address, std::size_t return Watch::Get().CollectArmed(address, address + bytes, written, context); #elif defined(__APPLE__) if (bytes == 0 || address + bytes < address) return false; - return Watch::Get().Collect(address, address + bytes, written, context); + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Armed); #else static_cast(address); static_cast(bytes); diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 378c67050..045b6c10b 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -41,13 +41,14 @@ struct Region { std::uintptr_t Address(std::size_t index) const { return reinterpret_cast(base) + index * page; } - std::set Collect() { + using Collector = bool (*)(std::uintptr_t, std::size_t, void (*)(void*, std::uintptr_t, std::uintptr_t), void*); + std::set Collect(Collector collector = &GuestWriteWatchCollect_nid_postfix, std::size_t first = 0, std::size_t count = 0) { std::set pages; struct Sink { Region* region; std::set* pages; } sink{this, &pages}; - const bool complete = GuestWriteWatchCollect_nid_postfix(Address(0), bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) { + const bool complete = collector(Address(first), count == 0 ? bytes - first * page : count * page, [](void* context, std::uintptr_t begin, std::uintptr_t end) { auto& target = *static_cast(context); for (auto at = begin; at < end; at += target.region->page) target.pages->insert((at - target.region->Address(0)) / target.region->page); }, &sink); @@ -83,6 +84,29 @@ void CheckCollect() { Require(region.Collect() == std::set({0, 1}), "a write across a page boundary is not reported on both pages"); } +void CheckFresh() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix) == std::set({4, 5, 6, 7}), "a fresh collection does not report exactly the never collected pages"); + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix).empty(), "a fresh collection reports pages again"); + region.base[6 * region.page] = 1; + Require(region.Collect() == std::set({1, 6}), "a fresh collection does not arm the pages it reports"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + +void CheckArmed() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + region.base[6 * region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectArmed_nid_postfix) == std::set({1}), "an armed collection does not report exactly the written armed pages"); + Require(region.Collect() == std::set({4, 5, 6, 7}), "an armed collection takes never collected pages"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + void CheckRegisterAgain() { Region region(4); GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); @@ -208,9 +232,11 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, register, mapped, host, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); + else if (mode == "fresh") CheckFresh(); + else if (mode == "armed") CheckArmed(); else if (mode == "register") CheckRegisterAgain(); else if (mode == "mapped") CheckRegisterMapped(); else if (mode == "host") CheckHostWrite(); diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index f98461a10..0824ee500 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(write_watch_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode collect register mapped host protection threads) +foreach(mode collect fresh armed register mapped host protection threads) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() From 4c7d83a2bf17fe460af7b4cdeddee410010b5323 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 00:58:30 +0200 Subject: [PATCH 137/277] feat(libSceHttp): implement sceHttpAbortWaitRequest and the unlimited sceHttpWaitRequest Deathloop (PPSA01670) imports sceHttpAbortWaitRequest; Planet Coaster (PPSA01736) and Palworld import sceHttpWaitRequest. Epoll handles are kept in a registry with an abort flag, a destroyed flag and a condition variable. A wait with a negative timeout blocks until sceHttpAbortWaitRequest, sceHttpDestroyEpoll or the sceHttpTerm of the last context wakes it; a bounded wait still returns 0 events. An abort stays set and makes every later wait return SCE_HTTP_ERROR_ABORTED (0x80431080); a wait woken by destroy or term returns 0 events. sceHttpDestroyEpoll no longer frees a handle a waiter is blocked on. sceHttpInit registers its context, and the epoll calls and sceHttpTerm return BEFORE_INIT without a live context and INVALID_ID for an unknown context or handle. Behaviour and error codes: shadPS4 src/core/libraries/network/http.cpp and http_error.h at a8a70da595da2a96e0410309472ba4a7eb558144. Signature (void* eh): Orbital tools/ida/db_types.json at b321fcdbd700cc02da1cb1ad25412781d5ec73c6 and SharpProspero src/SharpProspero/Interop/Net/Http.cs at 60977b29103452b9681ae10c7a9011d44965bf16. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 766997df3f8329fd6e8e7aac2b5d83daf88c8292) --- core/libs/SceTypes.hpp | 2 +- core/libs/prx/libSceHttp/CMakeLists.txt | 3 +- core/libs/prx/libSceHttp/Export.cpp | 96 ++++++++++++++++++--- core/libs/prx/libSceHttp/src/HttpErrors.hpp | 3 + core/libs/tests/GuestHttp.cpp | 61 ++++++++++++- docs/dev/TechnicalDebt.md | 3 +- 6 files changed, 149 insertions(+), 19 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..f7405fa94 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1253,7 +1253,7 @@ union NetCtlInfo { std::uint8_t opaque[256]; }; using NetCtlCallback = void (*)(int, void*); -struct HttpEpoll {}; +struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*); using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*); diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 5c930a4e2..48a098bec 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceHttp) if(BUILD_TESTING) add_test_executable(guest_http_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp.cpp) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_http_tests PRIVATE libSceHttp) + target_link_libraries(guest_http_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_tests) add_test(NAME guest_http COMMAND guest_http_tests) add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 3b9216532..7e0e9872f 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -5,12 +5,16 @@ #include "prx/libSceHttp/src/HttpErrors.hpp" #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. @@ -23,6 +27,28 @@ struct RequestStatusRegistration { static std::mutex g_statusMutex; static std::map g_statusCallbacks; +struct HttpEpoll { + std::mutex mutex; + std::condition_variable wake; + bool aborted = false; + bool destroyed = false; +}; + +static std::mutex g_stateMutex; +static std::unordered_set g_contexts; +static std::unordered_map> g_epolls; + +static std::shared_ptr FindEpoll(HttpEpollHandle eh) { + const auto it = g_epolls.find(eh); + return it == g_epolls.end() ? nullptr : it->second; +} + +static void MarkDestroyed(HttpEpoll& epoll) { + std::lock_guard lock(epoll.mutex); + epoll.destroyed = true; + epoll.wake.notify_all(); +} + extern "C" { int APS5_VABI sceHttpAbortRequest(int request_id) { @@ -55,9 +81,13 @@ int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int e } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - (void)http_ctx_id; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; if (!eh) return ERROR_INVALID_VALUE; - *eh = new HttpEpoll{}; + auto epoll = std::make_shared(); + *eh = epoll.get(); + g_epolls.emplace(epoll.get(), std::move(epoll)); return 0; } @@ -102,8 +132,17 @@ int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { } int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { - (void)http_ctx_id; - delete eh; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + g_epolls.erase(eh); + } + MarkDestroyed(*epoll); return 0; } @@ -131,7 +170,10 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s (void)memid; (void)ssl_ctx_id; (void)pool_size; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_stateMutex); + g_contexts.insert(id); + return id; } int APS5_VABI sceHttpsDisableOption(int id, uint32_t ssl_flags) { @@ -248,7 +290,16 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - (void)http_ctx_id; + std::vector> epolls; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; + if (!g_contexts.empty()) return 0; + for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); + g_epolls.clear(); + } + for (const auto& epoll : epolls) MarkDestroyed(*epoll); return 0; } @@ -258,10 +309,19 @@ int APS5_VABI sceHttpUnsetEpoll(int id) { } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { - if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; - if (timeout < 0) NotImplemented_nid_no_patch(__func__); - std::this_thread::sleep_for(std::chrono::microseconds(timeout)); - return 0; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::unique_lock lock(epoll->mutex); + const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; + if (timeout < 0) epoll->wake.wait(lock, woken); + else epoll->wake.wait_for(lock, std::chrono::microseconds(timeout), woken); + return epoll->aborted ? ERROR_ABORTED : 0; } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { @@ -434,8 +494,18 @@ int APS5_VABI sceHttpSetRedirectCallback(int id, HttpRedirectCallback cbfunc, vo return 0; } -int APS5_VABI sceHttpAbortWaitRequest(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::lock_guard lock(epoll->mutex); + epoll->aborted = true; + epoll->wake.notify_all(); return 0; } } diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp index b95f8d25b..1b18ec3e1 100644 --- a/core/libs/prx/libSceHttp/src/HttpErrors.hpp +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -1,8 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP #define CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP +inline constexpr int ERROR_BEFORE_INIT = static_cast(0x80431001); inline constexpr int ERROR_OUT_OF_MEMORY = static_cast(0x80431022); inline constexpr int ERROR_NETWORK = static_cast(0x80431063); +inline constexpr int ERROR_ABORTED = static_cast(0x80431080); +inline constexpr int ERROR_INVALID_ID = static_cast(0x80431100); inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); inline constexpr int ERROR_PARSE_HTTP_INVALID_RESPONSE = static_cast(0x80432060); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 9d18283f0..6183f70b8 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,9 +1,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include +#include extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); @@ -18,6 +21,9 @@ int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); int APS5_VABI sceHttpWaitRequest(HttpEpollHandle, HttpNBEvent*, int, int); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle); +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); int APS5_VABI sceHttpReadData(int, void*, std::size_t); int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); @@ -199,16 +205,65 @@ int main() { Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + constexpr int beforeInit = static_cast(0x80431001); + constexpr int aborted = static_cast(0x80431080); + constexpr int invalidId = static_cast(0x80431100); HttpEpollHandle epoll = nullptr; - Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); - Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); HttpNBEvent events[2]{}; + Require(sceHttpCreateEpoll(1, &epoll) == beforeInit); + Require(sceHttpAbortWaitRequest(nullptr) == beforeInit); + Require(sceHttpTerm_nid_postfix(1) == beforeInit); + const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(context > 0); + Require(sceHttpCreateEpoll(-1, &epoll) == invalidId); + Require(sceHttpCreateEpoll(context, nullptr) == invalidValue); + Require(sceHttpCreateEpoll(context, &epoll) == 0 && epoll != nullptr); Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); Require(sceHttpWaitRequest(epoll, events, 2, 1000) == 0); Require(sceHttpWaitRequest(epoll, nullptr, 2, 0) == invalidValue); Require(sceHttpWaitRequest(epoll, events, 0, 0) == invalidValue); Require(sceHttpWaitRequest(nullptr, events, 2, 0) == invalidValue); - Require(sceHttpDestroyEpoll(1, epoll) == 0); + const auto unknownEpoll = reinterpret_cast(&events); + Require(sceHttpAbortWaitRequest(nullptr) == invalidValue); + Require(sceHttpAbortWaitRequest(unknownEpoll) == invalidId); + Require(sceHttpWaitRequest(unknownEpoll, events, 2, -1) == invalidId); + Require(sceHttpDestroyEpoll(context, nullptr) == invalidValue); + Require(sceHttpDestroyEpoll(context, unknownEpoll) == invalidId); + Require(sceHttpDestroyEpoll(-1, epoll) == invalidId); + + auto waitWokenBy = [&](auto wake) { + std::atomic started{false}; + std::atomic result{1}; + std::thread waiter([&] { + started = true; + result = sceHttpWaitRequest(epoll, events, 2, -1); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(result == 1); + wake(); + waiter.join(); + return result.load(); + }; + Require(waitWokenBy([&] { Require(sceHttpAbortWaitRequest(epoll) == 0); }) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, -1) == aborted); + Require(sceHttpDestroyEpoll(context, epoll) == 0); + Require(sceHttpAbortWaitRequest(epoll) == invalidId); + Require(sceHttpDestroyEpoll(context, epoll) == invalidId); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + Require(waitWokenBy([&] { Require(sceHttpDestroyEpoll(context, epoll) == 0); }) == 0); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + const int otherContext = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(otherContext > 0 && otherContext != context); + Require(sceHttpTerm_nid_postfix(otherContext) == 0); + Require(sceHttpTerm_nid_postfix(otherContext) == invalidId); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); + Require(waitWokenBy([&] { Require(sceHttpTerm_nid_postfix(context) == 0); }) == 0); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == beforeInit); + Require(sceHttpTerm_nid_postfix(context) == beforeInit); char data[16]; Require(sceHttpReadData(1, data, sizeof(data)) == network); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..711887192 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -335,7 +335,7 @@ Throughout the project, every function at every stage either **does exactly what - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes -- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws +- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies @@ -364,6 +364,7 @@ Throughout the project, every function at every stage either **does exactly what - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - 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. - [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 +- [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws From 776cff0efb3c96082c71b29e636a7107e53b9ccb Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:40:29 +0000 Subject: [PATCH 138/277] fix(libSceHttp): use native Windows condition waits Use an SRW lock and native Windows condition variable for epoll waits, preserving the shared handle lifecycle. This avoids the scheduler-tick overshoot measured with std::condition_variable on Windows. Suggested-by: winstrex --- core/libs/prx/libSceHttp/Export.cpp | 79 ++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 7e0e9872f..b2d1bfbce 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -1,5 +1,9 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" @@ -27,9 +31,68 @@ struct RequestStatusRegistration { static std::mutex g_statusMutex; static std::map g_statusCallbacks; +#ifdef _WIN32 +struct HostMutex { + SRWLOCK native = SRWLOCK_INIT; + + HostMutex() = default; + HostMutex(const HostMutex&) = delete; + HostMutex& operator=(const HostMutex&) = delete; + + void lock() { AcquireSRWLockExclusive(&native); } + void unlock() { ReleaseSRWLockExclusive(&native); } +}; +#else +using HostMutex = std::mutex; +#endif + +class HostCondition { +public: + HostCondition() = default; + HostCondition(const HostCondition&) = delete; + HostCondition& operator=(const HostCondition&) = delete; + + void NotifyAll() { +#ifdef _WIN32 + WakeAllConditionVariable(&native); +#else + native.notify_all(); +#endif + } + + void Wait(std::unique_lock& lock) { +#ifdef _WIN32 + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait(lock); +#endif + } + + void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { +#ifdef _WIN32 + const auto now = std::chrono::steady_clock::now(); + if (deadline <= now) return; + const auto milliseconds = std::chrono::ceil(deadline - now).count(); + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && + GetLastError() != ERROR_TIMEOUT) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait_until(lock, deadline); +#endif + } + +private: +#ifdef _WIN32 + CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; +#else + std::condition_variable native; +#endif +}; + struct HttpEpoll { - std::mutex mutex; - std::condition_variable wake; + HostMutex mutex; + HostCondition wake; bool aborted = false; bool destroyed = false; }; @@ -46,7 +109,7 @@ static std::shared_ptr FindEpoll(HttpEpollHandle eh) { static void MarkDestroyed(HttpEpoll& epoll) { std::lock_guard lock(epoll.mutex); epoll.destroyed = true; - epoll.wake.notify_all(); + epoll.wake.NotifyAll(); } extern "C" { @@ -319,8 +382,12 @@ int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxe } std::unique_lock lock(epoll->mutex); const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; - if (timeout < 0) epoll->wake.wait(lock, woken); - else epoll->wake.wait_for(lock, std::chrono::microseconds(timeout), woken); + if (timeout < 0) { + while (!woken()) epoll->wake.Wait(lock); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); + while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); + } return epoll->aborted ? ERROR_ABORTED : 0; } @@ -505,7 +572,7 @@ int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { } std::lock_guard lock(epoll->mutex); epoll->aborted = true; - epoll->wake.notify_all(); + epoll->wake.NotifyAll(); return 0; } } From e56a7273752b79b6b0c4276883f330c5722ad548 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:40:36 +0000 Subject: [PATCH 139/277] feat(build): support Linux AddressSanitizer builds --- CMakeLists.txt | 13 ++++++++++++- core/libs/prx/libc/src/GuestArena.cpp | 13 +++++++++---- core/libs/tests/GuestMemory.cpp | 26 +++++++++++++++++++------- docs/dev/BUILD.md | 1 + 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..a48a0b337 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ if(BUILD_TESTING) endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) +option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) add_compile_definitions(APS5_ENABLE_TIMING_LOG=$) @@ -32,7 +33,17 @@ include(cmake/SystemDeps.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(NOT APPLE) +if(ANYPS5_ENABLE_ASAN) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") + endif() + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN requires GCC or Clang") + endif() + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + add_compile_definitions(ANYPS5_ENABLE_ASAN=1) +elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index fea67b8cc..356f465c9 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -32,10 +32,15 @@ std::atomic commitGeneration{1}; std::atomic privateMappingObserver{nullptr}; #endif -constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; -constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; -constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; -constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +#ifdef ANYPS5_ENABLE_ASAN +constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; +#else +constexpr std::uintptr_t GuestArenaOffset = 0; +#endif +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { return (value + alignment - 1) & ~(static_cast(alignment) - 1); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..5bccb6d1e 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -813,8 +813,10 @@ static void CheckGuestModuleImageProtection() { static void CheckUnhintedKernelMappingsLandInTheWindow() { constexpr std::size_t page = 0x4000; - constexpr std::uintptr_t windowStart = 0x200000000ull; - constexpr std::uintptr_t windowEnd = 0xFC00000000ull; + std::uintptr_t windowStart = 0; + std::size_t windowSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&windowStart, &windowSize); + const auto windowEnd = windowStart + windowSize; std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); void* direct = nullptr; @@ -889,11 +891,17 @@ static void CheckArenaHonoursHintsWithoutGoingBelowThem() { static void CheckFixedMappingsReachTheApplicationAreaEnd() { constexpr std::size_t page = 0x4000; - constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull; std::uintptr_t base = 0; std::size_t size = 0; GuestArena::GuestArenaRange_nid_postfix(&base, &size); - Require(base == 0x200000000ull && base + size == applicationAreaEnd); + constexpr std::uintptr_t expectedOffset = +#ifdef ANYPS5_ENABLE_ASAN + 0x0000100000000000ull; +#else + 0; +#endif + constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull + expectedOffset; + Require(base == 0x200000000ull + expectedOffset && base + size == applicationAreaEnd); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); void* const requested = reinterpret_cast(applicationAreaEnd - page * 2); @@ -919,7 +927,7 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); } -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -943,7 +951,7 @@ static void CheckMlock() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0); auto* bytes = static_cast(mapped); Require(sceKernelMlock_nid_postfix(mapped, 0) == 0); -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) const auto lockedBefore = LockedKilobytes(); #endif Require(sceKernelMlock_nid_postfix(bytes + 1, length - page) == 0); @@ -954,7 +962,7 @@ static void CheckMlock() { Require(GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &limits) && minimum >= length && maximum > minimum); Require(VirtualUnlock(mapped, length)); Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); -#elif defined(__linux__) +#elif defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(sceKernelMlock_nid_postfix(mapped, length) == 0); @@ -963,10 +971,14 @@ static void CheckMlock() { Require(sceKernelMlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == invalid); void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(reserved, page) == outOfMemory); +#endif Require(sceKernelMunmap(reserved, page) == 0); Require(sceKernelMunmap(mapped, length) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(mapped, page) == outOfMemory); +#endif } static void CheckSharedDirectMemoryLifecycle() { diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 50e7023fe..15cfe9803 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -57,6 +57,7 @@ Project switches accept `ON` or `OFF`: | `-DBUILD_TESTING=ON` | `OFF` | Build and register tests. | | `-DANYPS5_RELINKER_ONLY=ON` | `OFF` | Build only the relinker and its tests, without third-party dependencies. | | `-DANYPS5_ENABLE_SPIRV_TOOLS=ON` | `OFF` | Enable SPIR-V validation and optimization. | +| `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | From 4891d625af21c4870f12ffc358464dd894eb1c0a Mon Sep 17 00:00:00 2001 From: overlordxrz-source Date: Sun, 11 Oct 2026 15:30:03 +0000 Subject: [PATCH 140/277] feat(libSceNet): export sce_net_in6addr_linklocal_allnodes Co-authored-by: eliasbruno124-dev <230422461+eliasbruno124-dev@users.noreply.github.com> --- core/libs/prx/libSceNet/Export.cpp | 1 + core/libs/tests/GuestSceNet.cpp | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0775ad06b..d70156fea 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -369,6 +369,7 @@ std::int64_t message_length(const NetMsghdr* message) { extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; +alignas(std::uint32_t) extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16] = {0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; int* APS5_VABI sceNetErrnoLoc(void) { return errno_slot(); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e71c5d65c..1579e6645 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -46,6 +46,7 @@ int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); int APS5_VABI sceNetEpollDestroy(int); int APS5_VABI sceNetEpollAbort(int, int); 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 sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); @@ -255,6 +256,21 @@ static bool HostIpv6LoopbackUnavailable() { #endif } +static void CheckLinkLocalAllNodesIpv6() { + Require(reinterpret_cast(sce_net_in6addr_linklocal_allnodes) % alignof(std::uint32_t) == 0); + std::array address{}; + Require(sceNetInetPton(28, "ff02::1", address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); + std::array text; + text.fill('x'); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 7) == nullptr && *sceNetErrnoLoc() == 28); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 8) == text.data()); + Require(std::strcmp(text.data(), "ff02::1") == 0 && text[8] == 'x'); + address.fill(0xa5); + Require(sceNetInetPton(28, text.data(), address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -319,6 +335,7 @@ int main() { CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + CheckLinkLocalAllNodesIpv6(); CheckUnspecifiedIpv6(); const int listener = sceNetSocket(nullptr, 2, 1, 6); From a0a71559648c7647d17a69e059b8853e65d3709f Mon Sep 17 00:00:00 2001 From: Tharun Balaji Date: Sun, 11 Oct 2026 11:51:31 +0530 Subject: [PATCH 141/277] test(libkernel): cover creat error cases Carry the additional GuestFilesystem checks from issue #3303 after its implementation landed on main. The declaration already exists on main, so keep one prototype. --- core/libs/tests/GuestFilesystem.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..1bb19aaf9 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -443,6 +443,20 @@ int main() { Require(sceKernelOpen("", 0, 0) == static_cast(0x80020002u)); Require(open_nid_postfix(nullptr, 0, 0) == -1 && *__error_nid_postfix() == 14); Require(_open_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + const auto created = root / "created.txt"; + const int createdDescriptor = creat_nid_postfix(created.string().c_str(), 0644); + Require(createdDescriptor >= 0); + Require(write_nid_postfix(createdDescriptor, "created", 7) == 7 && close_nid_postfix(createdDescriptor) == 0); + Require(std::filesystem::file_size(created) == 7); + const int truncatedDescriptor = creat_nid_postfix(created.string().c_str(), 0600); + Require(truncatedDescriptor >= 0 && close_nid_postfix(truncatedDescriptor) == 0); + Require(std::filesystem::file_size(created) == 0); + Require(unlink_nid_postfix(created.string().c_str()) == 0); + Require(creat_nid_postfix((root / "absent" / "child.txt").string().c_str(), 0644) == -1 && + *__error_nid_postfix() == 2); + Require(creat_nid_postfix(rootName.c_str(), 0644) == -1 && *__error_nid_postfix() == 21); + Require(creat_nid_postfix("", 0644) == -1 && *__error_nid_postfix() == 2); + Require(creat_nid_postfix(nullptr, 0644) == -1 && *__error_nid_postfix() == 14); Require(rmdir_nid_postfix(rootName.c_str()) == -1 && *__error_nid_postfix() == 66); Require(sceKernelRmdir(rootName.c_str()) == static_cast(0x80020042u)); Require(rmdir_nid_postfix(presentName.c_str()) == -1 && *__error_nid_postfix() == 20); From b00efdb261fae754e2fb18d0f2e541d1082bf98d Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:45:26 -0400 Subject: [PATCH 142/277] fix(shader): accept abs, neg and op_sel source modifiers on v_cvt_pknorm_i16_f16 and v_cvt_pknorm_u16_f16 v_cvt_pknorm_i16_f16 and v_cvt_pknorm_u16_f16 with |x|, -x or op_sel in the VOP3 encoding stop the recompiler with "VOP3 source modifiers are not implemented", although the RDNA2 hardware executes them. These two opcodes now use the two-source f16 modifier path of v_pack_b32_f16: op_sel picks the high half of either source, then abs and neg apply. Other op_sel bits, clamp and omod are still refused. Matches 15632 of 15632 rows measured on gfx1036 (Radeon 610M) in two float modes, 0 differing. Co-Authored-By: Claude Haiku 5.5 --- .../VCvtPknormF16SourceModifiers.cpp | 174 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 12 +- 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp new file mode 100644 index 000000000..4e90961e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp @@ -0,0 +1,174 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +constexpr std::uint32_t RowCount = 8; +constexpr std::uint32_t VariantCount = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd712000au, 0x00020b04u, + 0xd713000bu, 0x00020b04u, 0xe0701000u, 0x80010a03u, 0xe0701004u, 0x80010b03u, 0xbf810000u, +}; + +struct Variant { + const char* operands; + std::uint32_t abs; + std::uint32_t neg; + std::uint32_t opSel; +}; + +constexpr Variant Variants[VariantCount] = { + {"|v4|, v5", 1, 0, 0}, + {"v4, |v5|", 2, 0, 0}, + {"-v4, v5", 0, 1, 0}, + {"v4, -v5", 0, 2, 0}, + {"-|v4|, v5", 1, 1, 0}, + {"v4, -|v5|", 2, 2, 0}, + {"v4, v5 op_sel:[1,0,0]", 0, 0, 1}, + {"v4, v5 op_sel:[0,1,0]", 0, 0, 2}, +}; + +constexpr std::uint32_t Rows[RowCount][2] = { + {0x00000400u, 0x00000400u}, + {0x000083ffu, 0x000083ffu}, + {0x00003555u, 0x0000d640u}, + {0x0000bc00u, 0x00003c00u}, + {0x00007bffu, 0x00000400u}, + {0xbeef0000u, 0xbeef3c00u}, + {0xbeef8000u, 0xbeef0000u}, + {0xbeef0000u, 0xbeeffc00u}, +}; + +constexpr std::uint32_t Expected[VariantCount][Results][RowCount] = { + { + {0x00020002u, 0xfffe0002u, 0x80012aa8u, 0x7fff7fffu, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040004u, 0x00000004u, 0x00005550u, 0xffffffffu, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020002u, 0x0002fffeu, 0x7fff2aa8u, 0x7fff8001u, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x7fff0000u}, + {0x00040004u, 0x00040000u, 0xffff5550u, 0xffff0000u, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffe0002u, 0x8001d558u, 0x7fff7fffu, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000004u, 0x00000000u, 0xffffffffu, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0x0002fffeu, 0x7fff2aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x7fff0000u}, + {0x00000004u, 0x00040000u, 0xffff5550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffefffeu, 0x8001d558u, 0x7fff8001u, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0xfffefffeu, 0x80012aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020000u, 0xfffe0000u, 0x80010000u, 0x7fff0000u, 0x00020000u, 0x7fff8001u, 0x00008001u, 0x80018001u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00000002u, 0x0000fffeu, 0x00002aa8u, 0x00008001u, 0x00007fffu, 0x80010000u, 0x80010000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, +}; + +constexpr const char* Opcodes[Results] = {"v_cvt_pknorm_i16_f16_e64 v10, ", "v_cvt_pknorm_u16_f16_e64 v11, "}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, const Variant& variant) { + const std::uint32_t first = 0xd7120000u | variant.opSel << 11u | variant.abs << 8u | 10u; + const std::uint32_t second = 0xd7130000u | variant.opSel << 11u | variant.abs << 8u | 11u; + const std::uint32_t sources = 0x00020b04u | variant.neg << 29u; + Code[5] = first; + Code[6] = sources; + Code[7] = second; + Code[8] = sources; + for (std::uint32_t index = 0; index < RowCount; ++index) { + std::copy(std::begin(Rows[index]), std::end(Rows[index]), &Input[index * Inputs]); + } + for (std::uint32_t index = RowCount * Inputs; index < Input.size(); ++index) Input[index] = 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* modeName, std::uint32_t variantIndex) { + for (std::uint32_t slot = 0; slot < Results; ++slot) { + for (std::uint32_t index = 0; index < RowCount; ++index) { + const std::uint32_t actual = Output[index * Results + slot]; + const std::uint32_t expected = Expected[variantIndex][slot][index]; + Require(actual == expected, + std::string("v_cvt_pknorm f16 source modifiers: ") + modeName + " row " + std::to_string(index) + " " + + Opcodes[slot] + Variants[variantIndex].operands + " of " + Hex(Rows[index][0]) + " and " + + Hex(Rows[index][1]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < VariantCount; ++index) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, Variants[index]); + Check("FLOAT_MODE 0xf0", index); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, Variants[index]); + Check("FLOAT_MODE 0xc0", index); + } + std::puts("v_cvt_pknorm f16 source modifiers 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 0e6078d53..d1749f283 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -738,6 +738,10 @@ bool isNativeVop3F16TernaryOpcode(RdnaOpcode opcode) { opcode == RdnaOpcode::VFmaF16; } +bool isNativeVop3F16PackOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VPackB32F16 || opcode == RdnaOpcode::VCvtPknormI16F16 || opcode == RdnaOpcode::VCvtPknormU16F16; +} + bool isNativeVop3I16TernaryOpcode(RdnaOpcode opcode) { return opcode == RdnaOpcode::VMed3I16 || opcode == RdnaOpcode::VMed3U16 || opcode == RdnaOpcode::VMax3I16 || opcode == RdnaOpcode::VMax3U16 || opcode == RdnaOpcode::VMin3I16 || opcode == RdnaOpcode::VMin3U16 || @@ -1862,7 +1866,7 @@ void applyNativeVop3B16BinaryModifiers(RdnaInstruction& instruction, std::uint32 instruction.destination.sdwaSel = (opSel & 0x8u) != 0u ? 5u : 4u; } -void applyNativeVop3PackB32F16Modifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { +void applyNativeVop3F16PackModifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { instruction.source0.opSel = (opSel & 0x1u) != 0u; instruction.source1.opSel = (opSel & 0x2u) != 0u; instruction.source0.negate = (neg & 0x1u) != 0u; @@ -2051,7 +2055,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, } return; } - if (opcode == RdnaOpcode::VPackB32F16) { + if (isNativeVop3F16PackOpcode(opcode)) { if ((abs & ~0x3u) != 0u || (opSel & ~0x3u) != 0u || clamp != 0u || omod != 0u || (neg & ~0x3u) != 0u) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } @@ -2383,8 +2387,8 @@ RdnaInstruction DecodeRdnaVop3(std::uint32_t programCounter, std::span Date: Sun, 11 Oct 2026 16:52:47 +0200 Subject: [PATCH 143/277] fix(libkernel): count guest module segments against the flexible memory budget sceKernelAvailableFlexibleMemorySize and the flexible maps counted only explicit flexible mappings, so the full 448 MiB was reported while the title's executable and modules were loaded. A title that derives its code and data size from the budget minus the available size got an unsigned underflow. The segments of the main executable and of every .guest.prx module that the title's ELF gives permissions now count, rounded to 16 KiB pages, as shadPS4 maps every non-system module segment as flexible memory. The relinker's own blocks and the host system libraries do not count. The charge ends when a module is unloaded. Refs #2883 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq --- core/libs/prx/libkernel/CMakeLists.txt | 8 +-- .../prx/libkernel/DirectMemory/Export.cpp | 11 +++- .../prx/libkernel/Module/src/ModuleInfo.cpp | 59 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 16 +++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..2280313f8 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -870,12 +870,12 @@ 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) + 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="$") add_test(NAME guest_memory COMMAND guest_memory_tests) if(WIN32) - 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="$") set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 91c0a1f6b..7deed55ab 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -30,6 +30,8 @@ #include #include +extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); + namespace { constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; @@ -90,9 +92,11 @@ size_t _flexibleUsedLocked() { } int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked())) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, _flexibleUsedLocked(), static_cast(FLEXIBLE_MEMORY_SIZE)); + 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)); return SCE_KERNEL_ERROR_ENOMEM; } const int result = DoMapAnon(addr, len, prot, flags); @@ -350,8 +354,9 @@ 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 images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked()); + *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, images + _flexibleUsedLocked()); return 0; } diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 66dc83653..8e5a508d4 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Module/EhFrame.hpp" #ifdef _WIN32 @@ -63,6 +64,11 @@ struct GuestImage { std::uintptr_t address; }; +std::uint64_t PageSpan(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = PS5_PAGE_SIZE - 1; + return ((address + size + pageMask) & ~pageMask) - (address & ~pageMask); +} + #ifndef _WIN32 template struct ImageSearch { @@ -162,6 +168,24 @@ std::vector GuestImages() { } return images; } + +int AddGuestSegmentBytes(dl_phdr_info* image, std::size_t, void* data) { + const std::string_view name = image->dlpi_name ? image->dlpi_name : ""; + if (!name.empty() && !name.ends_with(GuestModuleSuffix)) return 0; + constexpr auto allAccess = PF_R | PF_W | PF_X; + for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) { + const auto& header = image->dlpi_phdr[i]; + if (header.p_type != PT_LOAD || header.p_offset == 0 || (header.p_flags & allAccess) == allAccess) continue; + *static_cast(data) += PageSpan(header.p_vaddr, header.p_memsz); + } + return 0; +} + +std::uint64_t GuestSegmentBytes() { + std::uint64_t bytes = 0; + dl_iterate_phdr(AddGuestSegmentBytes, &bytes); + return bytes; +} #else bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) { return std::strncmp(reinterpret_cast(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0; @@ -267,6 +291,37 @@ std::vector GuestImages() { } return images; } + +std::uint64_t ImageSegmentBytes(std::uintptr_t base) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + static_cast(dos->e_lfanew)); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + bool relinked = false; + for (unsigned i = 0; i < sectionCount; ++i) relinked = relinked || SectionNamed(sections[i], ".elf0"); + std::uint64_t bytes = 0; + for (unsigned i = 0; i < sectionCount; ++i) { + const auto& section = sections[i]; + if (SectionProtection(section) == 0 || (relinked && std::strncmp(reinterpret_cast(section.Name), ".elf", 4) != 0)) continue; + bytes += PageSpan(section.VirtualAddress, section.Misc.VirtualSize); + } + return bytes; +} + +std::uint64_t GuestSegmentBytes() { + const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + constexpr std::wstring_view suffix = L".guest.prx"; + std::uint64_t bytes = 0; + for (const auto& image : GuestImages()) { + if (image.address != executable) { + const auto path = ModulePath(reinterpret_cast(image.address)); + if (path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL) + continue; + } + bytes += ImageSegmentBytes(image.address); + } + return bytes; +} #endif } @@ -323,4 +378,8 @@ int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) { return SCE_KERNEL_ERROR_ESRCH; } +std::uint64_t GuestImageSegmentBytes_nid_no_patch() { + return GuestSegmentBytes(); +} + } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..65c6f4e77 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -178,6 +178,21 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckGuestModuleUsesFlexibleMemory() { + constexpr std::size_t moduleData = 0x10000; + const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available + moduleData <= before); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, before, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && mapped == nullptr); + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckBatchMapStopsAtInvalidEntry() { constexpr std::size_t page = 0x4000; constexpr int mapFlexible = 3; @@ -1683,6 +1698,7 @@ int main() { CheckVirtualQueryPartialMunmap(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..16adb0b6a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -168,6 +168,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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)) - [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` and `UMTX_OP_WAKE_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. 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 60a7f28112525de8f77f90245b0e33a61c0a927f Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 17:19:42 +0200 Subject: [PATCH 144/277] feat(libkernel): implement sceKernelAioSetParam Dying Light 2 (PPSA02262) imports sceKernelAioSetParam (9WK-vhNXimw) from its eboot. In #1794 tv7 reported on main b5f5362d3 + #2129 (2026-10-10): FAIL: unresolved ELF import -q-vbO859Tw from libSceVideoOut.prx with "12 other imports are still unresolved on `main`. In the eboot, `-q-vbO859Tw`, `GTfJ64qFCPw`, `vwtVpHQKelI` (no known names), `sceKernelAioSetParam` and `sceNpWebApi2GetMemoryPoolStats`." The import is observed; its position in the loader queue after the nameless NIDs is predicted. The function fills one SceKernelAioSchedulingParam (five 4-byte fields, 20 bytes): Orbital cc408367 tools/ida/db_types.json:1103, SharpProspero 5dfdb4b0 Interop/Kernel/KernelAio.cs ("Sets every field of param at once") and ampr_emu cfa85df3 ps5/include/sce/_fs.h agree on (param, schedulingWindowSize, delayedCountLimit, enableSplit, splitSize, splitChunkSize). No source gives an error code, so a null struct and values outside SharpProspero's maximums (window and delayed count 0..128, split flag 0 or 1, sizes up to 0x1000000) throw. The engine does not use the settings, as sceKernelAioInitializeImpl ignores its struct; this is recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit e45a2b950afe594212db2764529ccae32f84be8c) --- core/libs/SceTypes.hpp | 8 +++++++ core/libs/prx/libkernel/Aio/src/Aio.cpp | 18 ++++++++++++++ core/libs/tests/GuestAio.cpp | 31 ++++++++++++++++++++++++- docs/dev/TechnicalDebt.md | 1 + 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..897e2407e 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -95,6 +95,14 @@ struct KernelAioRwRequest { std::int32_t fd; }; +struct KernelAioSchedulingParam { + std::int32_t schedulingWindowSize; + std::int32_t delayedCountLimit; + std::uint32_t enableSplit; + std::uint32_t splitSize; + std::uint32_t splitChunkSize; +}; + struct KernelBatchMapEntry { void* start; std::uint64_t offset; diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 040773e22..1d83f2eed 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -28,6 +28,10 @@ constexpr std::int32_t AioRequestNumMax = 128; constexpr std::int32_t AioIdNumMax = 128; constexpr std::uint32_t AioWaitAnd = 1; constexpr std::uint32_t AioWaitOr = 2; +constexpr std::int32_t AioSchedulingWindowMax = 128; +constexpr std::int32_t AioDelayedCountMax = 128; +constexpr std::uint32_t AioSplitSizeMax = 0x1000000; +constexpr std::uint32_t AioSplitChunkSizeMax = 0x1000000; std::mutex g_mutex; std::array g_states{}; @@ -204,6 +208,20 @@ void APS5_VABI sceKernelAioInitializeParam(void* param) { if (param == nullptr) throw std::invalid_argument("sceKernelAioInitializeParam: param is null"); } +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, int32_t schedulingWindowSize, int32_t delayedCountLimit, uint32_t enableSplit, uint32_t splitSize, uint32_t splitChunkSize) { + if (param == nullptr) throw std::invalid_argument("sceKernelAioSetParam: param is null"); + if (schedulingWindowSize < 0 || schedulingWindowSize > AioSchedulingWindowMax) throw std::runtime_error("sceKernelAioSetParam: a scheduling window outside 0..128 is not modelled"); + if (delayedCountLimit < 0 || delayedCountLimit > AioDelayedCountMax) throw std::runtime_error("sceKernelAioSetParam: a delayed count limit outside 0..128 is not modelled"); + if (enableSplit > 1) throw std::runtime_error("sceKernelAioSetParam: a split flag other than 0 or 1 is not modelled"); + if (splitSize > AioSplitSizeMax || splitChunkSize > AioSplitChunkSizeMax) throw std::runtime_error("sceKernelAioSetParam: a split size above 0x1000000 is not modelled"); + param->schedulingWindowSize = schedulingWindowSize; + param->delayedCountLimit = delayedCountLimit; + param->enableSplit = enableSplit; + param->splitSize = splitSize; + param->splitChunkSize = splitChunkSize; + return 0; +} + int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { (void)prio; return SubmitCommands(req, size, false, id); diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 0bbc3a952..e1a51c74e 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -22,6 +22,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size); void APS5_VABI sceKernelAioInitializeParam(void* param); +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, std::int32_t schedulingWindowSize, std::int32_t delayedCountLimit, std::uint32_t enableSplit, std::uint32_t splitSize, std::uint32_t splitChunkSize); int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioPollRequest(std::int32_t id, std::int32_t* state); @@ -48,7 +49,35 @@ int main() { const auto file = root / "data.bin"; const std::string seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; { std::ofstream stream(file, std::ios::binary); stream << seed; } - Check(sceKernelAioInitializeImpl(nullptr, 0) == 0); + static_assert(sizeof(KernelAioSchedulingParam) == 20); + std::array schedulingParams{}; + schedulingParams.fill({7, 7, 7, 7, 7}); + Check(sceKernelAioSetParam(&schedulingParams[1], 64, 16, 1, 0x200000, 0x40000) == 0); + Check(schedulingParams[1].schedulingWindowSize == 64 && schedulingParams[1].delayedCountLimit == 16); + Check(schedulingParams[1].enableSplit == 1 && schedulingParams[1].splitSize == 0x200000 && schedulingParams[1].splitChunkSize == 0x40000); + Check(schedulingParams[0].schedulingWindowSize == 7 && schedulingParams[0].splitChunkSize == 7); + Check(schedulingParams[2].schedulingWindowSize == 7 && schedulingParams[2].splitChunkSize == 7); + Check(sceKernelAioSetParam(&schedulingParams[2], 128, 128, 0, 0x1000000, 0x1000000) == 0); + Check(schedulingParams[2].schedulingWindowSize == 128 && schedulingParams[2].delayedCountLimit == 128); + Check(schedulingParams[2].enableSplit == 0 && schedulingParams[2].splitSize == 0x1000000 && schedulingParams[2].splitChunkSize == 0x1000000); + const auto setParamThrows = [&](std::int32_t window, std::int32_t delayed, std::uint32_t split, std::uint32_t size, std::uint32_t chunk) { + KernelAioSchedulingParam untouchedParam{7, 7, 7, 7, 7}; + bool thrown = false; + try { sceKernelAioSetParam(&untouchedParam, window, delayed, split, size, chunk); } catch (const std::exception&) { thrown = true; } + return thrown && untouchedParam.schedulingWindowSize == 7 && untouchedParam.delayedCountLimit == 7 && + untouchedParam.enableSplit == 7 && untouchedParam.splitSize == 7 && untouchedParam.splitChunkSize == 7; + }; + Check(setParamThrows(129, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(-1, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 129, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, -1, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 2, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x1000001, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x100000, 0x1000001)); + bool nullThrew = false; + try { sceKernelAioSetParam(nullptr, 32, 32, 1, 0x100000, 0x100000); } catch (const std::invalid_argument&) { nullThrew = true; } + Check(nullThrew); + Check(sceKernelAioInitializeImpl(schedulingParams.data(), static_cast(sizeof(schedulingParams))) == 0); int initParam = 0; sceKernelAioInitializeParam(&initParam); const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..11caf38ae 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -386,6 +386,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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 +- [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 - [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 From d85d1c268fdc6b681f61850d18f3a4348d2ae9ef Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:51:17 +0400 Subject: [PATCH 145/277] fix(relinker): validate program layout ranges before writing --- core/relinker/CMakeLists.txt | 15 ++++++ .../general/ProgramHeaderLayoutBuilder.cpp | 25 ++++++++-- .../tests/ProgramLayoutRangeTests.cpp | 49 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..8ecae1f1f 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -154,6 +154,21 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ) add_test(NAME cli_args COMMAND cli_args_tests) + add_test_executable(program_layout_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/ProgramLayoutRangeTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ) + target_include_directories(program_layout_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME program_layout_ranges COMMAND program_layout_ranges_tests) + set_tests_properties(program_layout_ranges PROPERTIES TIMEOUT 20) + add_test_executable(buffer_bounds_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/BufferBoundsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index d70c3385d..cd7ca36df 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -111,6 +112,8 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( if (ph.Type != PT_LOAD) continue; foundLoad = true; + if (ph.MemorySize > std::numeric_limits::max() - ph.MappedAddress) + throw Domain::RelinkerException("Load segment virtual range overflowed"); const std::uint64_t vaddrEnd = ph.MappedAddress + ph.MemorySize; if (vaddrEnd > highestVaddrEnd) highestVaddrEnd = vaddrEnd; @@ -156,22 +159,36 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( "Not enough program header slots: need " + std::to_string(neededPh) + ", available " + std::to_string(request.PhNum)); - const std::uint64_t headerBlockSize = request.PhOff + static_cast(neededPh) * request.PhEntSize; + const auto checkedEnd = [](std::uint64_t start, std::uint64_t size) { + if (size > std::numeric_limits::max() - start) + throw Domain::RelinkerException("Program header layout range overflowed"); + return start + size; + }; + if (request.PhEntSize < 56) + throw Domain::RelinkerException("Program header entry is too small"); + const std::uint64_t headerBlockSize = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); + if (headerBlockSize > buf.size()) + throw Domain::RelinkerException("Program header table exceeds the output file"); + if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) + throw Domain::RelinkerException("Extra block exceeds the output file"); std::uint64_t headerBlockAlign = kDefaultLoadAlignment; for (const auto& ph : request.OriginalHeaders) { if (ph.Type == PT_LOAD && !_segmentFilter->ShouldSkip(ph) && ph.Alignment > headerBlockAlign && (ph.Alignment & (ph.Alignment - 1)) == 0) headerBlockAlign = ph.Alignment; } const std::uint64_t headerBlockVaddr = - (request.ExtraBlockVaddr + request.ExtraBlockSize + headerBlockAlign - 1) & ~(headerBlockAlign - 1); + checkedEnd(checkedEnd(request.ExtraBlockVaddr, request.ExtraBlockSize), headerBlockAlign - 1) & ~(headerBlockAlign - 1); + checkedEnd(headerBlockVaddr, headerBlockSize); if (request.DynamicSegmentOffset < request.ExtraBlockOffset) throw Domain::RelinkerException("Dynamic segment offset lies before the extra block"); - if (request.DynamicSegmentOffset + request.DynamicSegmentSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.DynamicSegmentOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + request.DynamicSegmentSize > request.ExtraBlockSize - (request.DynamicSegmentOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Dynamic segment does not fit within the extra block"); if (request.InterpOffset < request.ExtraBlockOffset) throw Domain::RelinkerException("Interp offset lies before the extra block"); - if (request.InterpOffset + request.InterpSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.InterpOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + request.InterpSize > request.ExtraBlockSize - (request.InterpOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Interp data does not fit within the extra block"); const std::uint64_t dynamicSegmentVaddr = request.ExtraBlockVaddr + (request.DynamicSegmentOffset - request.ExtraBlockOffset); diff --git a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp new file mode 100644 index 000000000..c2059ece9 --- /dev/null +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +class KeepAll : public Elfpatcher::ISegmentFilter { +public: + bool ShouldSkip(const Domain::ProgramHeader&) const override { return false; } +}; + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Program layout range validation failed"); } + +int main() { + try { + Elfpatcher::ProgramHeaderLayoutBuilder builder(std::make_shared(), std::make_shared()); + constexpr auto maximum = std::numeric_limits::max(); + Domain::ProgramHeader load{}; + load.Type = 1; load.MappedAddress = maximum - 3; load.MemorySize = 8; + bool rejected = false; + try { builder.ComputeExtraBlockVaddr({load}, 0x1000); } catch (const std::exception&) { rejected = true; } + Require(rejected); + load.MappedAddress = 0x1000; load.MemorySize = 0x1000; + Require(builder.ComputeExtraBlockVaddr({load}, 0x1003) == 0x2003); + Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 6, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; + std::vector output(0x1100, 0xA5); + Require(builder.WriteLayout(output, valid) == 6); + for (int kind = 0; kind < 9; ++kind) { + auto request = valid; + switch (kind) { + case 0: request.PhEntSize = 55; break; + case 1: request.PhOff = maximum - 10; break; + case 2: request.PhOff = 0x10F0; break; + case 3: request.ExtraBlockVaddr = maximum - 10; break; + case 4: request.DynamicSegmentSize = maximum; break; + case 5: request.InterpSize = maximum; break; + case 6: request.DynamicSegmentOffset = maximum; break; + case 7: request.InterpOffset = maximum; break; + case 8: request.ExtraBlockOffset = maximum; break; + } + std::vector bytes(0x1100, 0xA5); + const auto before = bytes; + rejected = false; + try { builder.WriteLayout(bytes, request); } catch (const std::exception&) { rejected = true; } + Require(rejected && bytes == before); + } + std::cout << "12 program layout cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From 653eb704c2f01f0fd17b1b914fcadedef44eb90f Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:52:18 +0400 Subject: [PATCH 146/277] fix(relinker): validate complete allocated section ranges --- core/relinker/CMakeLists.txt | 16 +++++++ .../src/general/SectionHeaderTableBuilder.cpp | 30 +++++++++--- .../elfpatcher/tests/SectionRangeTests.cpp | 47 +++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 core/relinker/elfpatcher/tests/SectionRangeTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..8e9e1071e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -191,6 +191,22 @@ if(BUILD_TESTING) add_test(NAME linux_desktop_entry COMMAND linux_desktop_entry_tests) endif() + add_test_executable(section_header_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/SectionRangeTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(section_header_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME section_header_ranges COMMAND section_header_ranges_tests) + set_tests_properties(section_header_ranges PROPERTIES TIMEOUT 20) + add_test_executable(amd64_only_converter_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/Amd64OnlyConverterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index e9ee612df..916a1575e 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace Elfpatcher { @@ -43,18 +44,33 @@ void SectionHeaderTableBuilder::WriteTable( const auto phOffset = Io::ReadU64(buf, kEhdrPhOffOffset); const auto phCount = Io::ReadU16(buf, kEhdrPhNumOffset); const auto phStride = Io::ReadU16(buf, kEhdrPhEntSizeOffset); - const auto mappedAddress = [&](std::uint64_t offset) { + if (phStride < 56 || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) + throw std::runtime_error("Output program header table is out of bounds"); + const auto mappedAddress = [&](std::uint64_t offset, std::uint64_t sectionSize) { + if (offset > buf.size() || sectionSize > buf.size() - offset) + throw std::runtime_error("Allocated output section exceeds file"); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phStride; if (Io::ReadU32(buf, header) != PT_LOAD) continue; const auto start = Io::ReadU64(buf, header + 8); const auto size = Io::ReadU64(buf, header + 32); - if (offset >= start && offset - start < size) - return Io::ReadU64(buf, header + 16) + offset - start; + if (offset >= start && offset - start <= size && sectionSize <= size - (offset - start)) { + const auto relative = offset - start; + const auto address = Io::ReadU64(buf, header + 16); + if (relative > std::numeric_limits::max() - address || + sectionSize > std::numeric_limits::max() - (address + relative)) + throw std::runtime_error("Allocated output section address overflowed"); + return address + relative; + } } throw std::runtime_error("Allocated output section is outside load segments"); }; + const auto dynStrAddress = mappedAddress(request.DynStrOffset, request.DynStrSize); + const auto dynSymAddress = mappedAddress(request.DynSymOffset, request.DynSymSize); + const auto dynamicAddress = mappedAddress(request.DynamicSegmentOffset, request.DynamicSegmentSize); + const auto stubAddress = mappedAddress(request.StubOffset, request.StubSize); + const auto shStrTabOff = static_cast(buf.size()); constexpr std::uint64_t shStrTabSize = sizeof(kShStrTabBlob); for (const char i : kShStrTabBlob) @@ -65,10 +81,10 @@ void SectionHeaderTableBuilder::WriteTable( _writeSectionHeader(buf, shOff + kShdrEntrySize * 0, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, shStrTabOff, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, mappedAddress(request.DynStrOffset), request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, mappedAddress(request.DynSymOffset), request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, mappedAddress(request.DynamicSegmentOffset), request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, mappedAddress(request.StubOffset), request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, dynStrAddress, request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, dynSymAddress, request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, dynamicAddress, request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, stubAddress, request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); _byteWriter->WriteU64(buf, kEhdrShOffOffset, shOff); _byteWriter->WriteU16(buf, kEhdrShEntSizeOffset, static_cast(kShdrEntrySize)); diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp new file mode 100644 index 000000000..5332f80e5 --- /dev/null +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Section range validation failed"); } + +static std::vector Fixture() { + std::vector bytes(512, 0); + Io::WriteU64(bytes, 32, 64); Io::WriteU16(bytes, 54, 56); Io::WriteU16(bytes, 56, 1); + Io::WriteU32(bytes, 64, 1); Io::WriteU64(bytes, 72, 128); + Io::WriteU64(bytes, 80, 0x1000); Io::WriteU64(bytes, 96, 256); + return bytes; +} + +int main() { + try { + Elfpatcher::SectionHeaderTableBuilder builder(std::make_shared()); + const Elfpatcher::SectionHeaderTableRequest valid{128, 8, 136, 24, 160, 16, 176, 16}; + auto bytes = Fixture(); builder.WriteTable(bytes, valid); + const auto table = Io::ReadU64(bytes, 40); + Require(Io::ReadU64(bytes, table + 2 * 64 + 16) == 0x1000); + auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; + bytes = Fixture(); builder.WriteTable(bytes, empty); + Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); + for (int kind = 0; kind < 8; ++kind) { + bytes = Fixture(); auto request = valid; + switch (kind) { + case 0: request.DynStrSize = 257; break; + case 1: request.DynSymSize = std::numeric_limits::max(); break; + case 2: request.DynamicSegmentOffset = 383; request.DynamicSegmentSize = 2; break; + case 3: request.StubOffset = 384; request.StubSize = 1; break; + case 4: Io::WriteU16(bytes, 54, 1); break; + case 5: Io::WriteU64(bytes, 32, std::numeric_limits::max()); break; + case 6: Io::WriteU64(bytes, 80, std::numeric_limits::max() - 4); break; + case 7: Io::WriteU16(bytes, 56, 20); break; + } + const auto before = bytes; + bool rejected = false; + try { builder.WriteTable(bytes, request); } catch (const std::exception&) { rejected = true; } + Require(rejected && bytes == before); + } + std::cout << "10 section range cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From b02881cf0e34d92f5f52afdfcf274d42464e82a1 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:42:03 +0300 Subject: [PATCH 147/277] fix(libkernel): fail pthread_rwlock_unlock with EPERM when the rwlock is not held scePthreadRwlockUnlock called unlock_shared on the host mutex whenever the caller was not the writer, which is undefined behaviour for an unlocked rwlock or one write-locked by another thread. Track the reader count and return EPERM like FreeBSD libthr when the caller does not own the write lock and no read lock is held. --- core/libs/prx/libkernel/Pthread/src/Rwlock.cpp | 11 ++++++++--- core/libs/tests/GuestPthreadRwlock.cpp | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 7689b9c76..cd8890dcd 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -7,6 +7,7 @@ #include static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; @@ -105,10 +106,14 @@ int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); lock->_lock.unlock(); - } else { - lock->_readers.fetch_sub(1, std::memory_order_release); - lock->_lock.unlock_shared(); + return SCE_OK; } + if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{}) return SCE_KERNEL_ERROR_EPERM; + int readers = lock->_readers.load(std::memory_order_acquire); + do { + if (readers == 0) return SCE_KERNEL_ERROR_EPERM; + } while (!lock->_readers.compare_exchange_weak(readers, readers - 1, std::memory_order_acq_rel, std::memory_order_acquire)); + lock->_lock.unlock_shared(); return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index ffae6559a..b19f8646e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -29,6 +29,7 @@ using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EBUSY = 16; static constexpr int GUEST_EINVAL = 22; @@ -223,6 +224,12 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Holder reader{&rwlock, false}; Start(reader); @@ -242,6 +249,7 @@ int main() { Holder writer{&rwlock, true}; Start(writer); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); From 700f017eb6ba434f3535b0969bc25d7ffb917499 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:49:23 +0300 Subject: [PATCH 148/277] test(libkernel): check that timed-out rwlock reads leave no reader count --- core/libs/tests/GuestPthreadRwlock.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index b19f8646e..ff27c538e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -261,6 +261,7 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == 0); Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); bool rejected = false; try { pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); } From e5c280fed4fcebc792f1f7d98a54120b13d7d71d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:56:00 +0400 Subject: [PATCH 149/277] fix(codegen): reject rewrites inside instructions --- core/relinker/CMakeLists.txt | 16 +++++++++++ .../src/x86/X64InstructionRewriter.cpp | 15 ++++++++--- .../codegen/tests/RewriteBoundaryTests.cpp | 27 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 core/relinker/codegen/tests/RewriteBoundaryTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..c47fc2c5a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -144,6 +144,22 @@ if(BUILD_TESTING) target_include_directories(autorun_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include) add_test(NAME autorun_exit_status COMMAND autorun_exit_tests) set_tests_properties(autorun_exit_status PROPERTIES TIMEOUT 20) + add_test_executable(rewrite_boundaries_tests + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RewriteBoundaryTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(rewrite_boundaries_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME rewrite_boundaries COMMAND rewrite_boundaries_tests) + set_tests_properties(rewrite_boundaries PROPERTIES TIMEOUT 20) + add_test_executable(cli_args_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/CliArgsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp diff --git a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index a3899c3dd..212bdaee2 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -74,6 +74,18 @@ RewriteResult X64InstructionRewriter::Rewrite( throw CodegenException("Rewrite offset is out of bounds", request.Offset); } + const auto scanner = MakeInstructionScanner(); + const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); + bool instructionStart = false; + for (const auto& match : matches) { + if (match.Offset == request.Offset) { + instructionStart = true; + break; + } + } + if (!instructionStart) + throw CodegenException("Rewrite offset is not an instruction boundary", request.Offset); + const X64InstructionDecoder decoder; const std::size_t decodedLength = decoder.Decode(codeSection.data() + offset, codeSection.size() - offset); @@ -90,9 +102,6 @@ RewriteResult X64InstructionRewriter::Rewrite( rewritten.insert(rewritten.end(), request.NewBytes.begin(), request.NewBytes.end()); rewritten.insert(rewritten.end(), codeSection.begin() + offset + decodedLength, codeSection.end()); - const auto scanner = MakeInstructionScanner(); - const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); - std::vector adjustments; for (const auto& match : matches) { diff --git a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp new file mode 100644 index 000000000..fa9ee0055 --- /dev/null +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Invalid rewrite boundary accepted"); } + +int main() { + try { + const Codegen::X64InstructionRewriter rewriter; + for (const auto& code : {std::vector{0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3}}) { + const auto original = code; + for (std::size_t offset = 1; offset + 1 < code.size(); ++offset) { + bool rejected = false; + try { (void)rewriter.Rewrite(code, {offset, {0x90}}); } + catch (const std::exception&) { rejected = true; } + Require(rejected && code == original); + } + const auto result = rewriter.Rewrite(code, {0, {0x90}}); + Require(result.Bytes == std::vector{0x90, 0xC3}); + } + const auto result = rewriter.Rewrite({0x90, 0x90, 0xC3}, {1, {0x90, 0x90}}); + Require(result.Bytes == std::vector{0x90, 0x90, 0x90, 0xC3}); + std::cout << "18 rewrite boundary cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From 474c2b9ff1463d5569ed8661953c392804aeb252 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:56:55 +0400 Subject: [PATCH 150/277] fix(libkernel): reject malformed UTF-8 in param JSON --- .../AppMetadata/src/ParamJsonParser.cpp | 27 ++++++++++++++++++- core/libs/tests/ParamJsonParser.cpp | 23 ++++++++++++++++ core/relinker/CMakeLists.txt | 7 +++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3645c476d..4f515a105 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -95,6 +95,27 @@ std::uint32_t parseHex4(const std::string& text, std::size_t& position) { return value; } +void appendRawUtf8(const std::string& text, std::size_t& position, unsigned char first, std::string& result) { + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (first >= 0xC2 && first <= 0xDF) { count = 1; codepoint = first & 0x1F; minimum = 0x80; } + else if (first >= 0xE0 && first <= 0xEF) { count = 2; codepoint = first & 0x0F; minimum = 0x800; } + else if (first >= 0xF0 && first <= 0xF4) { count = 3; codepoint = first & 0x07; minimum = 0x10000; } + else throw std::runtime_error("invalid UTF-8 in param.json"); + if (count > text.size() - position) throw std::runtime_error("truncated UTF-8 in param.json"); + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(text[position + index]); + if ((byte & 0xC0) != 0x80) throw std::runtime_error("invalid UTF-8 continuation in param.json"); + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + throw std::runtime_error("invalid UTF-8 codepoint in param.json"); + result.push_back(static_cast(first)); + result.append(text, position, count); + position += count; +} + std::string parseJsonString(const std::string& text, std::size_t& position) { expectChar(text, position, '"'); std::string result; @@ -103,7 +124,11 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { const char c = text[position++]; if (c == '"') break; if (static_cast(c) < 0x20) throw std::runtime_error("control character in param.json string"); - if (c != '\\') { result.push_back(c); continue; } + if (c != '\\') { + if (static_cast(c) < 0x80) result.push_back(c); + else appendRawUtf8(text, position, static_cast(c), result); + continue; + } if (position >= text.size()) throw std::runtime_error("unterminated escape in param.json"); const char escape = text[position++]; switch (escape) { diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745..67d3e54ab 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -90,11 +90,34 @@ static void CheckNumberSyntax(const std::filesystem::path& path) { } } +static void CheckUtf8(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":")"; + const std::string suffix = R"("}}})"; + for (const auto& title : {std::string("ASCII"), std::string("\xC2\x80"), std::string("\xE3\x82\xB2\xE3\x83\xBC\xE3\x83\xA0"), + std::string("\xF0\x9F\x8E\xAE"), std::string("\xF4\x8F\xBF\xBF")}) { + Require(ParseText(path, prefix + title + suffix).title == title, "Valid UTF-8 title changed"); + } + Require(ParseText(path, prefix + R"(\uD83C\uDFAE)" + suffix).title == "\xF0\x9F\x8E\xAE", "Escaped title changed"); + for (const auto& title : {std::string("\x80"), std::string("\xC0\xAF"), std::string("\xC1\xBF"), std::string("\xC2"), + std::string("\xE0\x80\xAF"), std::string("\xED\xA0\x80"), std::string("\xE3\x82"), + std::string("\xF0\x80\x80\xAF"), std::string("\xF4\x90\x80\x80"), std::string("\xF5\x80\x80\x80"), + std::string("\xFE"), std::string("\xFF"), std::string("\xC2x"), std::string("\xE3\x82x")}) { + bool rejected = false; + try { ParseText(path, prefix + title + suffix); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 title"); + } + bool rejected = false; + try { ParseText(path, std::string("{\"") + "\x80" + R"(":0,)" + prefix.substr(1) + "Valid" + suffix); } + catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 object key"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); int result = 0; try { + CheckUtf8(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}, diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..d72b4764d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -393,6 +393,13 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) + add_test_executable(param_json_utf8_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) + target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) + add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) + set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp From 656005c2a4585e9941c17344e7e6efd813ba416e Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:57:45 +0400 Subject: [PATCH 151/277] fix(shader): handle cache file allocation failures --- CMakeLists.txt | 6 +++ .../recompiler/ShaderCacheDirectory.hpp | 30 +++++++++---- .../recompiler/tests/CacheFileReadTests.cpp | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 core/shader/recompiler/tests/CacheFileReadTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..f990873d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,12 @@ if(NOT APPLE) endif() if(BUILD_TESTING) + add_test_executable(shader_cache_file_read_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp) + target_include_directories(shader_cache_file_read_tests PRIVATE + ${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) 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/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index 229506482..010cfd2ee 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -4,11 +4,15 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include #include #include @@ -48,17 +52,27 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span& bytes) { bytes.clear(); - std::ifstream file(path, std::ios::binary | std::ios::ate); - if (!file) return false; - const auto size = static_cast(file.tellg()); - if (size < 0) return false; - file.seekg(0, std::ios::beg); - bytes.resize(static_cast(size)); - if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { + try { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) return false; + const auto size = static_cast(file.tellg()); + if (size < 0 || static_cast(size) > bytes.max_size() || + static_cast(size) > static_cast(std::numeric_limits::max())) + return false; + 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; + } + return true; + } catch (const std::bad_alloc&) { + bytes.clear(); + return false; + } catch (const std::length_error&) { bytes.clear(); return false; } - return true; } inline std::uint64_t HashBytes(std::span bytes, std::uint64_t seed = 0) { diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp new file mode 100644 index 000000000..29f5e61b3 --- /dev/null +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -0,0 +1,44 @@ +#include "ShaderCacheDirectory.hpp" +#include +#include +#include + +static bool FailLargeAllocation = false; + +void* operator new(std::size_t size) { + if (FailLargeAllocation && size > 1024 * 1024) throw std::bad_alloc(); + if (auto* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } +void* operator new[](std::size_t size) { return ::operator new(size); } +void operator delete[](void* memory) noexcept { ::operator delete(memory); } +void operator delete[](void* memory, std::size_t) noexcept { ::operator delete(memory); } + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Cache file read failed"); } + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-cache-read-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + int status = 0; + try { + std::vector bytes{std::byte{1}}; + Require(!ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + { std::ofstream file(path, std::ios::binary); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + const std::vector payload(2 * 1024 * 1024, 'S'); + { std::ofstream file(path, std::ios::binary | std::ios::trunc); file.write(payload.data(), payload.size()); Require(static_cast(file)); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.size() == payload.size()); + Require(bytes.front() == std::byte{'S'} && bytes.back() == std::byte{'S'}); + std::vector failed{std::byte{1}}; + FailLargeAllocation = true; + const auto read = ShaderRecompiler::ReadWholeFile(path, failed); + FailLargeAllocation = false; + Require(!read && failed.empty()); + std::cout << "5 cache file read cases passed\n"; + } catch (const std::exception& error) { FailLargeAllocation = false; std::cerr << error.what() << '\n'; status = 1; } + std::error_code error; + std::filesystem::remove(path, error); + return status; +} From 1e3a9345f0b5c013201e5e37154a68a6a51a9f01 Mon Sep 17 00:00:00 2001 From: mariusyo07 Date: Sun, 11 Oct 2026 18:02:39 +0200 Subject: [PATCH 152/277] fix(pad): accept vibration mode 3 Broken Sword Reforged (PPSA21480) calls scePadSetVibrationMode(handle, 3) after scePadOpen and treats a failure as fatal (ps5_controls.cpp:19 "Sony call failed with code 80920001"). Modes up to 3 are accepted, 4 and above stay invalid. Co-Authored-By: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> --- core/libs/prx/libScePad/Export.cpp | 2 +- core/libs/tests/GuestPad.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 38bb59ca5..59328e95f 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -236,7 +236,7 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { int APS5_VABI scePadSetVibrationMode(int handle, int mode) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (mode < 0 || mode > 2) return PAD_ERROR_INVALID_ARG; + if (mode < 0 || mode > 3) return PAD_ERROR_INVALID_ARG; Pad::SetVibrationMode(mode); return PAD_OK; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 992f0f7b9..70c716026 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -195,7 +195,8 @@ int main() { CheckInjectedState(path); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); - Require(scePadSetVibrationMode(handle, 3) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 3) == 0); + Require(scePadSetVibrationMode(handle, 4) == PAD_ERROR_INVALID_ARG); Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); From 4f013785dd7233f3f20a7cf6bba261479d8556be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:06:39 +0200 Subject: [PATCH 153/277] docs(debt): record unverified scePadSetVibrationMode mode 3 Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f08a8f8b0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -85,6 +85,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - only special ports (type 2) are handled, and they return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller; whether the console opens a handle and reports the device as disconnected later is unknown. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown +- [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 - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list - PPSA08427 imports declared without parameters, signatures unknown: [sceNpEntitlementAccessGetGameTrialsFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), imported by its bundled `pros.sdk.Prospero.prx` and not called before the main menu. Name from the aerolib NID list From 86bd3b6621633dfdcc2b07013abb5c31394fbcc2 Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Sun, 11 Oct 2026 21:40:58 +0530 Subject: [PATCH 154/277] feat(libSceAcm): implement sceAcm_FFT and sceAcm_Panner --- core/libs/CMakeLists.txt | 10 ++++++++ core/libs/prx/libSceAcm/Export.cpp | 6 ++--- core/libs/tests/GuestAcm.cpp | 41 ++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestAcm.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..4889bf240 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -329,4 +329,14 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + 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) + configure_windows_unwind(guest_acm_tests) + add_test(NAME guest_acm COMMAND guest_acm_tests) + if(WIN32) + set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index e62e5934d..fd80c9e1a 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -53,13 +53,11 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } -int APS5_VABI sceAcm_FFT() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAcm_FFT(void) { return 0; } -int APS5_VABI sceAcm_Panner() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAcm_Panner(void) { return 0; } diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp new file mode 100644 index 000000000..e360f8444 --- /dev/null +++ b/core/libs/tests/GuestAcm.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceAcmContextCreate(std::uint32_t*); +int APS5_VABI sceAcmContextDestroy(std::uint32_t); +int APS5_VABI sceAcmBatchStartBuffer(std::uint32_t, const void*, std::size_t, void*, std::uint32_t*); +int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* const[], void*, std::uint32_t*); +int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAcm_ConvReverb_SharedInput(void); +int APS5_VABI sceAcm_FFT(void); +int APS5_VABI sceAcm_Panner(void); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +int main() { + std::uint32_t context = 0; + Require(sceAcmContextCreate(nullptr) != 0); + Require(sceAcmContextCreate(&context) == 0 && context != 0); + + std::uint32_t batch = 0; + Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, nullptr) != 0); + Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, &batch) == 0 && batch != 0); + Require(sceAcmBatchWait(context, batch, 0) == 0); + + std::uint32_t batch2 = 0; + Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, nullptr) != 0); + Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); + Require(sceAcmBatchWait(context, batch2, 0) == 0); + + Require(sceAcm_ConvReverb_SharedInput() == 0); + Require(sceAcm_FFT() == 0); + Require(sceAcm_Panner() == 0); + + Require(sceAcmContextDestroy(context) == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..506be0dbf 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,6 +24,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported and device queries return no entries - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAcm](../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing - [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The layout and error codes are unverified on PS5 - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments From 6ae2a784afc05398a9450450bf60d40604b2ced7 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:20:59 -0400 Subject: [PATCH 155/277] test(libkernel): cover pthread key destructor lifetime --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../tests/GuestPthreadDestructorLifetime.cpp | 91 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 core/libs/tests/GuestPthreadDestructorLifetime.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..d83dceb20 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -496,6 +496,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_key PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_destructor_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadDestructorLifetime.cpp) + target_include_directories(guest_pthread_destructor_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_destructor_lifetime_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_destructor_lifetime_tests) + add_test(NAME guest_pthread_destructor_lifetime COMMAND guest_pthread_destructor_lifetime_tests) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_user_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserContext.cpp) target_include_directories(guest_user_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_user_context_tests PRIVATE libSceLibcInternal libkernel libc) diff --git a/core/libs/tests/GuestPthreadDestructorLifetime.cpp b/core/libs/tests/GuestPthreadDestructorLifetime.cpp new file mode 100644 index 000000000..9f10820f8 --- /dev/null +++ b/core/libs/tests/GuestPthreadDestructorLifetime.cpp @@ -0,0 +1,91 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +void APS5_VABI scePthreadExit(void* retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadGetthreadid(); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +void* APS5_VABI scePthreadGetspecific(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +} + +struct Context { + PthreadKey key = -1; + Pthread expected = nullptr; + int expectedId = 0; + unsigned calls = 0; + unsigned failures = 0; + bool exitExplicitly = false; + + void Check(bool passed, const char* message) { + if (passed) return; + std::fprintf(stderr, "%s: %s\n", exitExplicitly ? "explicit exit" : "return", message); + ++failures; + } +}; + +static void APS5_VABI DestroyValue(void* value) { + auto& context = *static_cast(value); + ++context.calls; + context.Check(scePthreadGetspecific(context.key) == nullptr, "key value was not cleared before its destructor"); + if (context.expected) { + const auto self = scePthreadSelf(); + context.Check(self == context.expected, "key destructor lost the exiting thread identity"); + context.Check(scePthreadGetthreadid() == context.expectedId, "key destructor lost the exiting thread ID"); + char name[32]{}; + context.Check(scePthreadGetname(self, name) == 0 && std::strcmp(name, "KeyDestructor") == 0, + "key destructor lost the exiting thread name"); + context.Check(sceKernelUsleep_nid_postfix(1000) == 0, "key destructor could not wait"); + } + context.Check(scePthreadSetspecific(context.key, value) == 0, "key destructor could not restore its value"); +} + +static void* APS5_VABI Worker(void* value) { + auto& context = *static_cast(value); + context.expected = scePthreadSelf(); + context.expectedId = scePthreadGetthreadid(); + context.Check(scePthreadSetspecific(context.key, value) == 0, "worker could not bind its key value"); + if (context.exitExplicitly) scePthreadExit(value); + return value; +} + +static bool RunGuest(bool exitExplicitly) { + Context context; + context.exitExplicitly = exitExplicitly; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + Pthread thread = nullptr; + if (scePthreadCreate(&thread, nullptr, Worker, &context, "KeyDestructor") != 0) return false; + void* result = nullptr; + context.Check(scePthreadJoin(thread, &result) == 0 && result == &context, "thread did not join with its return value"); + context.Check(context.calls == 4, "key destructor did not stop after four iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "key deletion failed"); + return context.failures == 0; +} + +static bool RunHost() { + Context context; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + std::thread worker([&] { + context.Check(scePthreadSetspecific(context.key, &context) == 0, "host worker could not bind its key value"); + }); + worker.join(); + context.Check(context.calls == 4, "host thread did not run four key destructor iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "host key deletion failed"); + return context.failures == 0; +} + +int main() { + const bool returned = RunGuest(false); + const bool exited = RunGuest(true); + const bool host = RunHost(); + if (!returned || !exited || !host) return 1; + std::puts("PASS: pthread key destructors retain thread identity on return and explicit exit; four iterations and host cleanup preserved"); +} From 503043ca003759c50e904b0a756b5352b52aa499 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:21:40 -0400 Subject: [PATCH 156/277] fix(libkernel): run pthread key destructors before thread release --- .../libkernel/Pthread/include/ThreadLifecycle.hpp | 1 + core/libs/prx/libkernel/Pthread/src/Thread.cpp | 2 ++ core/libs/prx/libkernel/Pthread/src/Tsd.cpp | 13 +++++++++++++ core/libs/tests/GuestPthreadDestructorLifetime.cpp | 11 ++++++++--- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp index f342bb0ca..68deb724f 100644 --- a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp +++ b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp @@ -8,6 +8,7 @@ public: static void SetThreadDtors(thread_dtors_func_t callback); static void SetThreadAtexitCount(get_thread_atexit_count_func_t callback); static void SetThreadAtexitReport(thread_atexit_report_func_t callback); + static void RunKeyDtors(); }; extern "C" void APS5_VABI scePthreadExit(void* retval); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 11400bdd2..ec4c30d9a 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -340,9 +340,11 @@ static void finishThread(PthreadPrivate* self, void* retval) { if (CurrentGuestState().finishing) throw std::runtime_error("Guest thread is already finishing"); CurrentGuestState().finishing = true; + CurrentGuestState().exiting = true; UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); + ThreadLifecycle::RunKeyDtors(); { std::unique_lock lk(self->_join_mtx); self->_retval = retval; diff --git a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp index f04da09b6..f9fa4ecc6 100644 --- a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp @@ -1,4 +1,5 @@ #include "../include/Pthread.hpp" +#include "../include/ThreadLifecycle.hpp" #include "../include/WindowsThreadLocal.hpp" #include "prx/libc/include/General.hpp" #include @@ -32,8 +33,15 @@ static std::uint64_t g_lastSequence = FREE_KEY; struct ThreadValues { std::array values{}; + bool destroyed = false; ~ThreadValues() { + RunDestructors(); + } + + void RunDestructors() { + if (destroyed) return; + destroyed = true; for (int round = 0; round < DESTRUCTOR_ITERATIONS; ++round) { bool called = false; for (int key = 0; key < MAX_KEYS; ++key) { @@ -52,6 +60,7 @@ struct ThreadValues { } if (!called) return; } + values = {}; } }; @@ -64,6 +73,10 @@ static ThreadValues& Values() { #endif } +void ThreadLifecycle::RunKeyDtors() { + Values().RunDestructors(); +} + extern "C" { int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor) { diff --git a/core/libs/tests/GuestPthreadDestructorLifetime.cpp b/core/libs/tests/GuestPthreadDestructorLifetime.cpp index 9f10820f8..4bddcb951 100644 --- a/core/libs/tests/GuestPthreadDestructorLifetime.cpp +++ b/core/libs/tests/GuestPthreadDestructorLifetime.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadCancel(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadGetthreadid(); @@ -24,6 +25,7 @@ struct Context { unsigned calls = 0; unsigned failures = 0; bool exitExplicitly = false; + bool pendingCancel = false; void Check(bool passed, const char* message) { if (passed) return; @@ -53,13 +55,15 @@ static void* APS5_VABI Worker(void* value) { context.expected = scePthreadSelf(); context.expectedId = scePthreadGetthreadid(); context.Check(scePthreadSetspecific(context.key, value) == 0, "worker could not bind its key value"); + if (context.pendingCancel) context.Check(scePthreadCancel(context.expected) == 0, "worker could not request cancellation"); if (context.exitExplicitly) scePthreadExit(value); return value; } -static bool RunGuest(bool exitExplicitly) { +static bool RunGuest(bool exitExplicitly, bool pendingCancel = false) { Context context; context.exitExplicitly = exitExplicitly; + context.pendingCancel = pendingCancel; if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; Pthread thread = nullptr; if (scePthreadCreate(&thread, nullptr, Worker, &context, "KeyDestructor") != 0) return false; @@ -85,7 +89,8 @@ static bool RunHost() { int main() { const bool returned = RunGuest(false); const bool exited = RunGuest(true); + const bool canceled = RunGuest(false, true); const bool host = RunHost(); - if (!returned || !exited || !host) return 1; - std::puts("PASS: pthread key destructors retain thread identity on return and explicit exit; four iterations and host cleanup preserved"); + if (!returned || !exited || !canceled || !host) return 1; + std::puts("PASS: pthread key destructors retain thread identity on return and explicit exit; pending cancellation, four iterations and host cleanup preserved"); } From b2da41e542ffb141bea004b1de57a57244c49705 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 17:58:39 +0200 Subject: [PATCH 157/277] test(libkernel): accept host stacks above the AMM range in guest_amm_placement On Windows, real thread stacks that exist before the guest arena is reserved can sit near the arena top, so the AMM range correctly lands 9-60 GiB below it and the 1 GiB bound failed about 1 run in 10. The test now requires that no free 64 GiB window exists above the AMM range. Co-Authored-By: Claude Fable 5.1 --- core/libs/tests/GuestAmmPlacement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp index 8faaad4c2..e52fe22b5 100644 --- a/core/libs/tests/GuestAmmPlacement.cpp +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -30,7 +30,7 @@ int main() { Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); Require(end == multimapStart && multimapEnd - start == 64ull << 30, "AMM ranges are not two adjacent 32 GiB ranges"); Require(start >= arenaBase && multimapEnd <= arenaEnd - occupied, "AMM ranges overlap the used top of the guest arena"); - Require(multimapEnd > arenaEnd - occupied - (1ull << 30), "AMM ranges are not at the top of the free guest arena"); + Require(GuestArena::GuestArenaHighestFree_nid_no_patch(64ull << 30, 2ull << 20) < start, "AMM ranges are not at the top of the free guest arena"); std::puts("AMM ranges placed below the used top of the guest arena"); return 0; } catch (const std::exception& error) { From 43783f725c93f867f945296f8eafff52c0316fdc Mon Sep 17 00:00:00 2001 From: Lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:28:46 +0200 Subject: [PATCH 158/277] feat(libSceAgc): build versioned ACB atomic GDS commands --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 13 ++- core/libs/prx/libSceAgc/tests/Commands.cpp | 97 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index de20362b0..a4692c619 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -11,10 +11,15 @@ extern "C" { -// unknown signature -void* APS5_VABI sceAgcAcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) | + ((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) | + ((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u); + return Agc::Command::Emit(buf, 0x1du, { + operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index, + mask & 0x00ff00ffu, data, static_cast(compareData), + static_cast(compareData >> 32u), static_cast(extraData), + static_cast(extraData >> 32u)}, __func__); } std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) { diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fd59ffa94..d3d9c7f83 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -37,6 +37,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); namespace { @@ -404,6 +405,101 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +struct AtomicGdsAllocation { + Storage storage; + std::uint32_t capacity = 15; + std::uint32_t calls = 0; + bool accepted = true; +}; + +bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) { + auto& allocation = *static_cast(userData); + check(count == 15, "atomic GDS allocation omitted packet or reserved words"); + ++allocation.calls; + if (!allocation.accepted) return false; + buffer->bottom = allocation.storage.words.data(); + buffer->top = buffer->bottom + allocation.capacity; + buffer->cursor_up = buffer->bottom; + buffer->cursor_down = buffer->top; + return true; +} + +void testAtomicGds() { + Storage storage; + storage.words.fill(0xdeadbeefu); + storage.buffer.cursor_up += 2; + auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu, + 0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull); + const std::array expected{ + 0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u, + 0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u}; + check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address"); + check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch"); + check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words"); + check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu && + std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }), + "atomic GDS wrote outside its packet"); + + const std::array, 9> controls{{ + {0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0}, + {0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u}, + {0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}}; + for (const auto& entry : controls) { + Storage controlStorage; + auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1], + 0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0); + std::array controlExpected{}; + controlExpected[0] = 0xc0091d00u; + controlExpected[1] = entry[2]; + check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch"); + } + + auto emit = [](CommandBuffer* buffer) { + return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + }; + Storage exact; + exact.buffer.cursor_down = exact.words.data() + 15; + exact.buffer.reserved_dw = 4; + check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS rejected an exact fit with reserved space"); + const auto exactBefore = exact.words; + expectFailure([&] { emit(&exact.buffer); }); + check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS overwrote reserved space"); + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 10; + const auto before = exhausted.words; + expectFailure([&] { emit(&exhausted.buffer); }); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), + "failed atomic GDS allocation modified the buffer"); + expectFailure([&] { emit(nullptr); }); + + Storage noDown; + noDown.buffer.top = noDown.words.data() + 11; + noDown.buffer.cursor_down = nullptr; + check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top, + "atomic GDS did not honor a buffer with no down cursor"); + + AtomicGdsAllocation allocation; + CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4}; + check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 && + growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback"); + const std::array zeroPacket{0xc0091d00u}; + check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()), + "atomic GDS callback allocation produced the wrong packet"); + for (const bool accepted : {false, true}) { + AtomicGdsAllocation failure; + failure.accepted = accepted; + failure.capacity = 14; + const auto failureBefore = failure.storage.words; + CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4}; + expectFailure([&] { emit(&failed); }); + check(failure.calls == 1 && failure.storage.words == failureBefore && + failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr), + "atomic GDS wrote a packet after failed or insufficient allocation"); + } +} + void testDebugRaiseException() { int result = -1; try { @@ -459,6 +555,7 @@ int main(int argc, char** argv) { testRegisterRange(); testPacketPayloadAddress(); testMemory(); + testAtomicGds(); testDebugRaiseException(); testDefaults(); LibcRunShutdown_nid_postfix(); From 54b9b93240a1dbcf3046425953453b5b8cccf121 Mon Sep 17 00:00:00 2001 From: Semir Kabir <66262518+semirkabir@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:29:28 -0700 Subject: [PATCH 159/277] feat(libSceUlt): add underscore mutex and queue create exports --- core/libs/prx/libSceUlt/Export.cpp | 16 ++++++++++++++++ core/libs/tests/GuestUltThread.cpp | 27 +++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 3c818ddb3..a0b1d880f 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -580,6 +580,22 @@ int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, buildVersion); } +int APS5_VABI _sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { + return sceUltMutexOptParamInitialize(optParam, buildVersion); +} + +int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { + return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, buildVersion); +} + +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { + return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, buildVersion); +} + +int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { + return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, buildVersion); +} + int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); } diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 26f5e126c..fdf347e64 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -22,6 +22,14 @@ int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uin int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*, std::uint32_t); int APS5_VABI sceUltSemaphoreAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreRelease(void*, std::int32_t); +int APS5_VABI _sceUltMutexOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltQueuePush(void*, const void*); +int APS5_VABI sceUltQueuePop(void*, void*); } static constexpr int Ok = 0; @@ -150,6 +158,25 @@ static void UnderscoredCreate() { CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); + alignas(8) static std::uint8_t mutexOptParam[128]; + alignas(8) static std::uint8_t mutex[256]; + std::memset(mutexOptParam, 0xFF, sizeof(mutexOptParam)); + CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0) == Ok); + CHECK(mutexOptParam[8] == 0 && mutexOptParam[15] == 0); + CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam, 0) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam, 0) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Ok); + alignas(8) static std::uint8_t dataPool[512]; + alignas(8) static std::uint8_t queue[512]; + CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Null); + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Ok); + CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr, 0) == Null); + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr, 0) == Ok); + std::uint64_t value = 0x1122334455667788ull, output = 0; + CHECK(sceUltQueuePush(queue, &value) == Ok); + CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); } int main(int argc, char** argv) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..8506d3cb3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -52,7 +52,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [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 -- PPSA01530, PPSA23827 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), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate and _sceUltSemaphoreCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to `sceUltWaitingQueueResourcePoolCreate` and `sceUltSemaphoreCreate`; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- PPSA01530, PPSA23827 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), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), 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. - [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. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [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 5f123fd1b23dab88b62bc2a7e70eba6838fc5c47 Mon Sep 17 00:00:00 2001 From: Semir Kabir <66262518+semirkabir@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:30:02 -0700 Subject: [PATCH 160/277] feat(libSceFiber): add the underscore thread frame pointer export --- core/libs/prx/libSceFiber/Export.cpp | 4 ++++ core/libs/tests/Fiber.cpp | 5 +++++ docs/dev/TechnicalDebt.md | 2 ++ 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 83572e7d1..5935f1bad 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -458,6 +458,10 @@ int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_poin return SCE_OK; } +int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_pointer) { + return sceFiberGetThreadFramePointerAddress(addr_frame_pointer); +} + int32_t APS5_VABI sceFiberStartContextSizeCheck(uint32_t flags) { if (flags != 0) return SCE_FIBER_ERROR_INVALID; bool expected = false; diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index da0721409..ccd01ed4a 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -18,6 +18,7 @@ std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); std::int32_t APS5_VABI sceFiberGetSelf(FiberObject**); std::int32_t APS5_VABI sceFiberGetInfo(FiberObject*, FiberInfo*); std::int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(std::uint64_t*); +std::int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(std::uint64_t*); std::int32_t APS5_VABI sceFiberStartContextSizeCheck(std::uint32_t); std::int32_t APS5_VABI sceFiberStopContextSizeCheck(void); } @@ -54,6 +55,9 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null"); Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer"); + framePointer = 0; + Require(_sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "underscore frame pointer null"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "underscore thread frame pointer"); volatile double carried = 1.5; std::uint64_t received = 0; Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber"); @@ -124,6 +128,7 @@ int main() { std::uint64_t returned = 0; std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "frame pointer outside a fiber"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "underscore frame pointer outside a fiber"); Require(RunFirst(&returned) == 0 && returned == 250, "run first until second returns"); Require(_mm_getcsr() == threadCsr, "thread MXCSR restored after the fibers"); std::thread([&] { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..7532750dc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -343,6 +343,8 @@ Throughout the project, every function at every stage either **does exactly what - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes +- [_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 + ### Functional - [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. From 5027dcedc71be71685b53d0d8a85c9daf5ef53ed Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 01:32:12 +0900 Subject: [PATCH 161/277] fix(shader): accept signed less-than in runtime SRT values #2072 taught the SRT evaluator SLessThan32, but IsRuntimeUniformOp still left it out, so the runtime validator rejected any descriptor dword computed through a signed comparison before the evaluator could compute it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++++ .../Optimization/src/SrtWalker/SrtInstructionPredicates.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f..791177978 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5,6 +5,7 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Optimization/SrtWalker.hpp" #include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" @@ -443,6 +444,9 @@ void verifySignedSrtComparison() { source.dwords[1] = &make(IrOpcode::SelectU32, IrType::U32, {&aboveMin, &greaterThanMin, &equalMin}); source.dwords[2] = &make(IrOpcode::SelectU32, IrType::U32, {&belowMaxValue, &belowMax, &equalMax}); plan.descriptorSources.push_back(source); + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { + require(SrtWalker().ValidateRuntimeValue(plan, source.dwords[dword]), "signed SRT comparison: the runtime validator rejects a descriptor dword the evaluator computes"); + } const std::array sourceIndex{0u}; const std::array>, 4> cases{{ {0x80000000u, {0xau, 0xdu, 0xeu}}, diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 0f3a3a6af..6ed2a7fdc 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -86,6 +86,7 @@ bool IsRuntimeUniformOp(IrOpcode opcode) { case IrOpcode::SelectU1: case IrOpcode::SelectU32: case IrOpcode::SelectF32: + case IrOpcode::SLessThan32: case IrOpcode::ULessThan32: case IrOpcode::IEqual32: case IrOpcode::UGreaterThan32: From 38cb3b12d19e999b922890193f99c24933c609eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:32:13 +0200 Subject: [PATCH 162/277] docs(technical-debt): record the single source of sceAgcAcbAtomicGds_0900 Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..ece4cccb9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -149,6 +149,7 @@ Throughout the project, every function at every stage either **does exactly what - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. +- [sceAgcAcbAtomicGds_0900](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws From 8d72065b68f0618dfd984a5b1775cd18fae621fb Mon Sep 17 00:00:00 2001 From: Eli Yarson <7914811+eliyarson@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:34:27 -0300 Subject: [PATCH 163/277] docs: record DOOM homebrew on Linux with an NVIDIA GPU --- docs/user/COMPATIBILITY.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index 2e667fa59..a6eb265f4 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -3,13 +3,14 @@ | Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 3.4GHz | Intel(R) HD Graphic 620 / i5-7200 2.5GHz | |----------------|-----------|-------------------|-------|------------------------------|------------------------------------------| | Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | 36 FPS | -| DOOM Shareware (homebrew) | PPSA99666 | In game (E1M1 and later maps) | In game (E1M1), software Vulkan only | ? | ? | +| DOOM Shareware (homebrew) | PPSA99666 | In game (E1M1 and later maps) | In game (E1M1) | ? | ? | DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit/ps5-doom), release v1.1.1 (content version 01.001.001). - Windows 11, RTX 4070 (version 01.001.000, an integration build before [#553](https://github.com/boykopovar/AnyPS5/pull/553), [#683](https://github.com/boykopovar/AnyPS5/pull/683), [#684](https://github.com/boykopovar/AnyPS5/pull/684) and [#685](https://github.com/boykopovar/AnyPS5/pull/685) were merged): movement and shooting in E1M1. [In-game screenshots](https://github.com/boykopovar/AnyPS5/pull/685#issuecomment-5997547697). - Windows 11, RTX 4070 SUPER (v1.1.1, `main` `2edaf7f4` plus open PRs, reported in [#690](https://github.com/boykopovar/AnyPS5/pull/690)): further maps, weapon switching, new games, level select, save/load round trip and audible audio. - Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. +- Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. - On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). -Not verified: Linux with a hardware GPU, current `main` without extra PRs. +Not verified: Windows on current `main` without extra PRs, Linux with AMD or Intel GPUs. From a03afed4c3b00473555504ab1a57687a25d54add Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:03:57 +0200 Subject: [PATCH 164/277] fix(exports): export the calls PPSA07411 imports PPSA07411 (Resident Evil 4) fails to load without these imports that no library exports. Their signatures are unknown, so they are declared without parameters and throw until they are implemented. Adds one test and a TechnicalDebt entry. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceConvertKeycode/Export.cpp | 5 ++ core/libs/prx/libSceJson2/Export.cpp | 21 +++++++ .../prx/libSceNpSessionSignaling/Export.cpp | 6 ++ core/libs/prx/libSceSsl/CMakeLists.txt | 9 +++ core/libs/prx/libSceSsl/Export.cpp | 30 +++++++++ core/libs/tests/GuestPPSA07411Exports.cpp | 62 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 7 files changed, 134 insertions(+) create mode 100644 core/libs/tests/GuestPPSA07411Exports.cpp diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 2ac60a0cd..ec43b33b0 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -27,4 +27,9 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { return 0; } +int APS5_VABI sceConvertKeycodeGetCharacter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 672d857ee..1a02b97bd 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -830,4 +830,25 @@ int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce4Json5ValuecvbEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index b28941f1b..b694abb6f 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -93,4 +93,10 @@ int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index d66b9716e..aa3508b58 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -12,4 +12,13 @@ if(BUILD_TESTING) set_tests_properties(guest_ssl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ppsa07411_exports_tests ${LIBS_INCLUDE_DIR}/tests/GuestPPSA07411Exports.cpp) + target_include_directories(guest_ppsa07411_exports_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ppsa07411_exports_tests PRIVATE libSceSsl libSceJson2 libSceNpSessionSignaling libSceConvertKeycode libc) + configure_windows_unwind(guest_ppsa07411_exports_tests) + add_test(NAME guest_ppsa07411_exports COMMAND guest_ppsa07411_exports_tests) + if(WIN32) + set_tests_properties(guest_ppsa07411_exports 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 e8fd49499..4b3e23a4e 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -159,4 +159,34 @@ 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 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/GuestPPSA07411Exports.cpp b/core/libs/tests/GuestPPSA07411Exports.cpp new file mode 100644 index 000000000..92acb9a86 --- /dev/null +++ b/core/libs/tests/GuestPPSA07411Exports.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslCreateConnection(); +int APS5_VABI sceSslDeleteConnection(); +int APS5_VABI sceSslEnableVerifyOption(); +int APS5_VABI sceSslConnect(); +int APS5_VABI sceSslRead(); +int APS5_VABI sceSslWrite(); +int APS5_VABI sceConvertKeycodeGetCharacter(); +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(); +int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(); +int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(); +int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(); +int APS5_VABI _ZNK3sce4Json5ValuecvbEv(); +} + +namespace { + +struct Export { + const char* name; + int (APS5_VABI *function)(); +}; + +const Export exports[] = { + {"sceSslCreateConnection", sceSslCreateConnection}, + {"sceSslDeleteConnection", sceSslDeleteConnection}, + {"sceSslEnableVerifyOption", sceSslEnableVerifyOption}, + {"sceSslConnect", sceSslConnect}, + {"sceSslRead", sceSslRead}, + {"sceSslWrite", sceSslWrite}, + {"sceConvertKeycodeGetCharacter", sceConvertKeycodeGetCharacter}, + {"sceNpSessionSignalingGetGroupFromSessionId", sceNpSessionSignalingGetGroupFromSessionId}, + {"_ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_", _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_}, + {"_ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_", _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_}, + {"_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_", _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_}, + {"_ZNK3sce4Json5ValuecvbEv", _ZNK3sce4Json5ValuecvbEv}, +}; + +bool ReportsNotImplemented(const Export& entry) { + try { + entry.function(); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == std::string(entry.name) + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", entry.name, error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", entry.name); + return false; +} + +} + +int main() { + bool correct = true; + for (const auto& entry : exports) correct &= ReportsNotImplemented(entry); + return correct ? 0 : 1; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..63faecf90 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -597,3 +597,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, 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) From a68bc9dfdcddd66259f4a27b4b389b29e7f039dd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:59:26 +0200 Subject: [PATCH 165/277] fix(libkernel): allow sceKernelMprotect on committed host memory outside the guest registry Thread stacks are host-private memory; the call threw instead of setting the native protection. Co-Authored-By: Claude Sonnet 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 4 ++++ core/libs/tests/GuestMemory.cpp | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 1fae6ec14..a31561d9b 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -882,6 +882,10 @@ int DoMprotect(const void* addr, size_t len, int prot) { else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase); else throw std::invalid_argument("Memory protection of a foreign image is not supported"); } + if (memory.Type == MEM_PRIVATE && memory.State == MEM_COMMIT && memory.RegionSize >= bytes && !mutation.Overlaps(pointer, bytes)) { + if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + return 0; + } #else mutation.RegisterMainImage(); #endif diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..8a60050dd 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1671,12 +1671,31 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); Require(start == top && size == static_cast(window)); } +#ifdef _WIN32 +static void CheckMprotectOfHostMemoryOutsideTheGuestRegistry() { + constexpr std::size_t length = 0x40000; + void* host = VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + Require(host != nullptr); + Require(sceKernelMprotect(host, length, 0x3) == 0); + static_cast(host)[length - 1] = 7; + Require(static_cast(host)[length - 1] == 7); + Require(sceKernelMprotect(host, length, 0x1) == 0); + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READONLY); + Require(sceKernelMprotect(host, length, 0x3) == 0); + Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READWRITE); + Require(VirtualFree(host, 0, MEM_RELEASE) != 0); +} +#endif int main() { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); #endif CheckAvailableDirectMemoryIsLargestFreeBlock(); +#ifdef _WIN32 + CheckMprotectOfHostMemoryOutsideTheGuestRegistry(); +#endif CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); From 63a45aacdf638e130f262fa93a3a7a144ad7ee32 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 16:35:54 +0000 Subject: [PATCH 166/277] docs: document Windows Dozen driver workaround --- docs/user/USAGE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..724532978 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -128,6 +128,15 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. +On Windows, the OpenCL, OpenGL, and Vulkan Compatibility Pack can install Mesa Dozen, which exposes D3D12 devices through Vulkan. Some D3D12 drivers can crash while the Vulkan loader enumerates adapters, even when AnyPS5 would select a native Vulkan device. If this happens and the system has another Vulkan driver, disable Dozen for the current PowerShell session before launching AnyPS5: + +```powershell +$env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' +.\app.exe +``` + +Do not disable Dozen if it is the only Vulkan driver available; the application would have no device to use. `VK_LOADER_DRIVERS_DISABLE` matches Vulkan driver manifest filenames and requires a Vulkan loader built with headers 1.3.234 or later ([loader documentation](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md#filter-environment-variables)). + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. From 3a3504850a9124df269316aa8766f1762c75f601 Mon Sep 17 00:00:00 2001 From: A-Akhil Date: Sun, 11 Oct 2026 22:06:22 +0530 Subject: [PATCH 167/277] fix(shader): take v_sin_f32 and v_cos_f32 from an exact series where the driver's result is more than 4 ulp off --- .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dc3fa2a70..646655287 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -746,6 +746,31 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); } +std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0xc17183a8u); + for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); +} + +std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0x40fce9c5u); + for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return polynomial; +} + +std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); +} + std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) { const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant); const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu)); @@ -757,9 +782,11 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, true); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); - const auto distance = Select(state, TypeF32(state), nearHalf, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute), absolute); + const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); const auto result = TrigLinearNearZero(state, value, distance, linearConstant); return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits)); @@ -768,8 +795,14 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); - const auto distance = Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute); + const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto low = CosTurnSeries(state, absolute); + const auto middle = SinTurnSeries(state, distance); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); + const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); + const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu)); } From d57d3f00007e9cd366bce3862d487b46e6bc0285 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 16:39:36 +0000 Subject: [PATCH 168/277] fix(agc): store pending results when queues go idle --- .../Execution/include/VulkanDevice.hpp | 1 + .../src/Driver/Queues/QueueWorker.cpp | 4 + .../Execution/src/VulkanDevice.cpp | 8 ++ .../Graphics/include/Texture.hpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 + .../tests/execution/StoreIdleResults.cpp | 120 ++++++++++++++++++ 6 files changed, 140 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 37a54bcdd..2255a232a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -57,6 +57,7 @@ public: // mutex through the flush hook, so a thread submitting on another queue's behalf (a WAIT_REG_MEM // poller, a compute worker's between-packet flush) passes false: it must only submit, never wait. void SubmitRecorded(bool reapFirst = true); + void StoreIdleResults(bool reapFirst = true); // Unlocked drain: SubmitAndEpoch (under the mutex) returns the serial covering every recorded // batch (0: nothing in flight); WaitRecorded(serial) waits for it WITHOUT the mutex (the caller // holds a shared_ptr to this device); ReapRecorded(serial) (under the mutex) runs the completions diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 9fa87efc4..1f72d6255 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -104,6 +104,10 @@ void Driver::run(std::uint32_t id) noexcept { while (!ready()) { if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) { + lock.unlock(); + if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(id == 0); + lock.lock(); + if (ready()) break; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); changed.wait(lock, ready); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87d211a49..f7f669f6d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1443,6 +1443,14 @@ bool OpportunisticReap() { } +void VulkanDevice::StoreIdleResults(bool reapFirst) { + if (!Graphics::StorageTexture::AnyPending()) return; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + Graphics::StorageTexture::FlushAllPending("idle"); + SubmitRecorded(reapFirst); +} + void VulkanDevice::SubmitRecorded(bool reapFirst) { if (!state->recorder) return; // Finished batches are retired first (a non-blocking fence status check on the front of the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9f4c7765a..38f9fc199 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -176,6 +176,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 bool AnyPending(); static bool StoreAtFlipRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow // retile) moves it too. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7a21d9720..45c82389e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2970,6 +2970,12 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +bool StorageTexture::AnyPending() { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + return pending.textures.begin() != pending.textures.end(); +} + bool StorageTexture::StoreAtFlipRequested(const char* value) { if (value == nullptr) return false; if (std::strcmp(value, "1") == 0) return true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp new file mode 100644 index 000000000..65bcb0d52 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 8; +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 = 0x60000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, 0x12345678u, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint32_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + 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(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto surface = AddressOf(texels); + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == 0x12345678u) return true; + } + return false; + }; + + Store(*device, texels); + Require(StorageTexture::FindPending(surface, bytes) != nullptr, "the store left no pending image"); + Require(!stored(), "the pending result reached guest memory before the idle store"); + + device->StoreIdleResults(); + Require(StorageTexture::FindPending(surface, bytes) == nullptr, "the idle store left the image pending"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(stored(), "the idle store did not bring the result to guest memory without a further submission"); + + device->StoreIdleResults(); + device->WaitIdle(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + std::puts("store idle results tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2f5d166e2beca897b523f6aa40bdcde99c521ed2 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:34:47 -0400 Subject: [PATCH 169/277] fix(shader): accept output modifiers on v_cvt_i16_f16 and ignore them as the hardware does v_cvt_i16_f16 with mul:2, mul:4, div:2 or clamp in the VOP3 encoding stops the recompiler with "VOP3 source modifiers are not implemented". On gfx1036 (Radeon 610M) the result is identical with and without mul:2, mul:4, div:2 and clamp combined with mul:2, for all 66 measured inputs; clamp on its own is not a measured case. The decoder now accepts omod and clamp on v_cvt_i16_f16 and drops them, as measured. Clamp without an output multiplier is accepted by the same rule: clamp with mul:2 gives the same hardware result as mul:2 alone. Matches 528 of 528 rows in two float modes, 0 differing. v_cvt_u16_f16 is not measured and stays refused. Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../execution/VCvtI16F16OutputModifiers.cpp | 130 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 3 +- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp new file mode 100644 index 000000000..b4517eaac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t RowCount = 12; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u, + 0xe0781000u, 0x80010a03u, 0xbf810000u, +}; + +struct Variant { + const char* modifiers; + std::uint32_t clamp; + std::uint32_t omod; +}; + +constexpr Variant Variants[] = { + {"mul:2", 0, 1}, + {"mul:4", 0, 2}, + {"div:2", 0, 3}, + {"clamp mul:2", 1, 1}, +}; + +constexpr std::uint32_t Rows[RowCount] = { + 0x00003c00u, + 0x0000bc00u, + 0x0000c100u, + 0x00004200u, + 0x00006400u, + 0x00005640u, + 0x0000d640u, + 0x00007bffu, + 0x0000fbffu, + 0x00007e00u, + 0xbeef3c00u, + 0xbeefc100u, +}; + +constexpr std::uint32_t Expected[RowCount] = { + 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, + 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, + 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) { + Code[7] = 0xd5d3000au | variant.clamp << 15u; + Code[8] = 0x00000104u | variant.omod << 27u; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid]; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode, const Variant& variant) { + for (std::uint32_t tid = 0; tid < RowCount; ++tid) { + const std::uint32_t actual = Output[tid * Results]; + Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) + + " of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Variant& variant : Variants) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant); + Check("M1", variant); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant); + Check("M2", variant); + } + std::puts("v_cvt_i16_f16 output modifiers 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 0e6078d53..eb6cea434 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2060,8 +2060,9 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, const bool sourceModifiers = supportsNativeVop3SourceModifiers(opcode); const bool resultModifiers = supportsNativeVop3ResultModifiers(opcode); const bool clampModifier = supportsNativeVop3Clamp(opcode); + const bool ignoresOutputModifiers = opcode == RdnaOpcode::VCvtI16F16; if (sourceModifiers) { - if (opSel != 0u || (omod != 0u && !resultModifiers) || (clamp != 0u && !clampModifier)) { + if (opSel != 0u || (omod != 0u && !resultModifiers && !ignoresOutputModifiers) || (clamp != 0u && !clampModifier && !ignoresOutputModifiers)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; From 2124cfba5f1bf0b60100f50dc6c4245cde5c1936 Mon Sep 17 00:00:00 2001 From: Semir Kabir <66262518+semirkabir@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:43:36 +0000 Subject: [PATCH 170/277] feat(libkernel): report the process affinity from cpuset_getaffinity --- .../prx/libkernel/Pthread/Posix/Thread.cpp | 12 ++++++--- .../prx/libkernel/Pthread/include/Pthread.hpp | 1 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 10 ++++++++ core/libs/tests/GuestAvailableCpumask.cpp | 25 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 11e577c3c..37beb1141 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -40,20 +40,26 @@ KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask) { - constexpr int LevelWhich = 3, WhichThread = 1; + constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; constexpr int GuestEfault = 14, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; if (size < sizeof(KernelCpumask) || size > MaximumSize) { *__error_nid_postfix() = GuestErange; return -1; } - if (level != LevelWhich || which != WhichThread || id != -1) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread"); + const bool thread = level == LevelWhich && which == WhichThread && id == -1; + const bool process = level == LevelWhich && which == WhichProcess && (id == -1 || id == getpid_nid_postfix()); + if (!thread && !process) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); if (!mask) { *__error_nid_postfix() = GuestEfault; return -1; } KernelCpumask affinity = 0; - scePthreadGetaffinity(scePthreadSelf(), &affinity); + if (thread) scePthreadGetaffinity(scePthreadSelf(), &affinity); + else { + scePthreadSelf(); + affinity = LiveGuestThreadsAffinity(); + } std::memset(mask, 0, size); std::memcpy(mask, &affinity, sizeof(affinity)); return 0; diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index f2df5c9b2..05177ac38 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -151,5 +151,6 @@ void NoteGuestStackProtection(std::uintptr_t start, std::size_t bytes, bool read int GuestThreadCpuClockId(const PthreadPrivate* thread); bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); +KernelCpumask LiveGuestThreadsAffinity(); #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 11400bdd2..e82077db5 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -240,6 +240,16 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask) { } } +KernelCpumask LiveGuestThreadsAffinity() { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + KernelCpumask mask = 0; + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire)) mask |= thread->affinity.load(std::memory_order_relaxed); + } + return mask; +} + bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index 6485fcffe..cf40f562b 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -198,6 +198,31 @@ int main() { Require(changed && restored && created && joined && inherited[0] == 0 && inherited[1] == last); Require(explicitSet && explicitCreated && explicitJoined && explicitAffinity[0] == 0 && explicitAffinity[1] == available); } + + std::uint64_t processSet[4] = {~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, sizeof(processSet), processSet) == 0); + Require(processSet[0] == available && processSet[1] == 0 && processSet[2] == 0 && processSet[3] == 0); + processSet[0] = 0; + Require(cpuset_getaffinity_nid_postfix(3, 2, getpid_nid_postfix(), 8, processSet) == 0 && processSet[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 4, processSet) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 8, nullptr) == -1 && *__error_nid_postfix() == GUEST_EFAULT); + if (first != last) { + std::promise park; + auto parked = park.get_future(); + Pthread pinned = nullptr; + Require(scePthreadCreate(&pinned, &attr, Worker, &parked, nullptr) == SCE_OK); + Require(scePthreadSetaffinity(pinned, first) == SCE_OK); + Require(scePthreadSetaffinity(scePthreadSelf(), last) == SCE_OK); + std::uint64_t unionSet[1] = {0}; + const int unionResult = cpuset_getaffinity_nid_postfix(3, 2, -1, 8, unionSet); + std::uint64_t ownSet[1] = {0}; + const int ownResult = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, ownSet); + Require(scePthreadSetaffinity(scePthreadSelf(), available) == SCE_OK); + park.set_value(); + Require(scePthreadJoin(pinned, nullptr) == SCE_OK); + Require(unionResult == 0 && unionSet[0] == (first | last)); + Require(ownResult == 0 && ownSet[0] == last); + } Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(ReadCpuset(pthread_self_nid_postfix()) == available); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..838cbdc14 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -406,6 +406,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [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 +- [cpuset_getaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. 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 - [dup and dup2](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` between a guest socket and a host descriptor in either direction, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates and the descriptor numbers come from the guest socket counter, not the lowest free number as in FreeBSD `kern_dup`. The errno values for descriptor exhaustion are not verified against the console. - [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. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. From 28962787b39f5a57236d0422ca996a10b2e06493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:44:14 +0200 Subject: [PATCH 171/277] docs(technical-debt): describe the v_sin and v_cos series fallback Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..5bb6907a1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -478,7 +478,7 @@ Throughout the project, every function at every stage either **does exactly what - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). -- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2Ï€ at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2Ï€ at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. From fba96d6422ce802aa2a79f72e3a5fd947dcb52a2 Mon Sep 17 00:00:00 2001 From: Alex <270190108+CouchRiv@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:07:47 -0400 Subject: [PATCH 172/277] fix(libc): asin(|x| > 1) returns the x86 default NaN and leaves errno unchanged On main asin(2.0) returns 0x7ff8000000000000 and sets errno to 33; FreeBSD msun (lib/msun/src/e_asin.c) returns 0xfff8000000000000 and never sets errno. asin now handles |x| > 1 itself with quiet comparisons, so a NaN input still raises no exception. For |x| <= 1 and for NaN the host function is still called. Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Math.cpp | 8 +++++++- core/libs/tests/GuestMath.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..aef6c0385 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -70,7 +70,13 @@ int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0 int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } -double APS5_VABI asin_nid_postfix(double x) { return std::asin(x); } +double APS5_VABI asin_nid_postfix(double x) { + if (std::isgreater(x, 1.0) || std::isless(x, -1.0)) { + std::feraiseexcept(FE_INVALID); + return -std::numeric_limits::quiet_NaN(); + } + return std::asin(x); +} double APS5_VABI acos_nid_postfix(double x) { return std::acos(x); } double APS5_VABI sqrt_nid_postfix(double x) { return std::sqrt(x); } float APS5_VABI sqrtf_nid_postfix(float x) { return std::sqrt(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..2179dd60a 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -40,6 +40,7 @@ int APS5_VABI __fpclassifyf_nid_postfix(float); float APS5_VABI fmodf_nid_postfix(float, float); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); +double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); @@ -281,6 +282,36 @@ static void CheckIntegerConversions() { static bool Near(double value, double expected, double tolerance) { return std::abs(value - expected) <= tolerance * std::abs(expected); } +static void CheckAsin() { + const double pi = 3.14159265358979323846; + const double pi_2 = pi / 2; + const double nan = std::numeric_limits::quiet_NaN(); + const double infinity = std::numeric_limits::infinity(); + + Require(asin_nid_postfix(0.) == 0.); + Require(std::signbit(asin_nid_postfix(-0.))); + Require(Near(asin_nid_postfix(1.), pi_2, 1e-15)); + Require(Near(asin_nid_postfix(-1.), -pi_2, 1e-15)); + Require(std::isnan(asin_nid_postfix(2.0)) && std::signbit(asin_nid_postfix(2.0))); + Require(std::isnan(asin_nid_postfix(-2.0)) && std::signbit(asin_nid_postfix(-2.0))); + Require(std::isnan(asin_nid_postfix(infinity)) && std::signbit(asin_nid_postfix(infinity))); + Require(std::isnan(asin_nid_postfix(-infinity)) && std::signbit(asin_nid_postfix(-infinity))); + Require(std::isnan(asin_nid_postfix(nan))); + Require(Near(asin_nid_postfix(0.5), pi/6, 1e-15)); + Require(Near(asin_nid_postfix(-0.5), -pi/6, 1e-15)); + Require(Near(asin_nid_postfix(1./std::sqrt(2)), pi/4, 1e-15)); + Require(asin_nid_postfix(1e-300) == 1e-300); + Require(asin_nid_postfix(-1e-300) == -1e-300); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 0; + asin_nid_postfix(2.0); + Require(std::fetestexcept(FE_INVALID)); + Require(errno == 0); + std::feclearexcept(FE_ALL_EXCEPT); + asin_nid_postfix(nan); + Require(!std::fetestexcept(FE_INVALID)); +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); @@ -478,6 +509,7 @@ int main() { Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); Require(__isnormal_nid_postfix(std::numeric_limits::denorm_min()) == 0); + CheckAsin(); const auto quotient = div_nid_postfix(7, 2); Require(quotient.quot == 3 && quotient.rem == 1); const auto negativeNumerator = div_nid_postfix(-7, 2); From a1e01e18507b43a91aad512070097faa090bb4c1 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:47:32 -0400 Subject: [PATCH 173/277] test(agc): cover storage upload lifetime before submission --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 143 ++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..3a4b4db86 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -775,6 +775,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only) + add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only) + set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_storage_baseline_settled PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a67d7e6c..f65914e97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2929,6 +2929,145 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { std::cout << "single-pass storage moves" << (watched ? " (watched)" : "") << ": ok (" << tested << " surfaces)\n"; } +class StorageUploadProbe { +public: + StorageUploadProbe(Context& context, bool failView) : context(context), resolver(context.deviceProc), failView(failView) { + Require(active == nullptr, "nested storage upload probe"); + active = this; + context.deviceProc = resolve; + } + ~StorageUploadProbe() { + for (const auto view : destroyedViews) function("vkDestroyImageView")(context.device, view, nullptr); + if (imageDestroyed) function("vkDestroyImage")(context.device, image, nullptr); + if (memoryFreed) function("vkFreeMemory")(context.device, memory, nullptr); + context.deviceProc = resolver; + active = nullptr; + } + VkImage Image() const { return image; } + bool Destroyed() const { return imageDestroyed || memoryFreed; } + bool Released() const { return imageDestroyed && memoryFreed; } + bool FailedView() const { return failedView; } + std::uint32_t Clears() const { return clears; } + +private: + template + TFunction function(const char* name) const { return reinterpret_cast(resolver(context.device, name)); } + static VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* created) { + const auto result = active->function("vkCreateImage")(device, info, callbacks, created); + if (result == VK_SUCCESS) active->image = *created; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL bindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) { + const auto result = active->function("vkBindImageMemory")(device, image, memory, offset); + if (result == VK_SUCCESS && image == active->image) active->memory = memory; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) { + if (info->image == active->image && active->failView && active->clears != 0) { + active->failedView = true; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + const auto result = active->function("vkCreateImageView")(device, info, callbacks, created); + if (result == VK_SUCCESS && info->image == active->image) active->views.push_back(*created); + return result; + } + static VKAPI_ATTR void VKAPI_CALL destroyView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) { + if (std::find(active->views.begin(), active->views.end(), view) != active->views.end()) active->destroyedViews.push_back(view); + else active->function("vkDestroyImageView")(device, view, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) { + if (image == active->image) active->imageDestroyed = true; + else active->function("vkDestroyImage")(device, image, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) { + if (memory == active->memory) active->memoryFreed = true; + else active->function("vkFreeMemory")(device, memory, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL clearImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* value, std::uint32_t count, const VkImageSubresourceRange* ranges) { + if (image == active->image) ++active->clears; + active->function("vkCmdClearColorImage")(commands, image, layout, value, count, ranges); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast(createImage); + if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast(bindImageMemory); + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(createView); + if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast(destroyView); + if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast(destroyImage); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(freeMemory); + if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast(clearImage); + return active->resolver(device, name); + } + Context& context; + PFN_vkGetDeviceProcAddr resolver; + bool failView; + bool failedView = false; + bool imageDestroyed = false; + bool memoryFreed = false; + std::uint32_t clears = 0; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; + std::vector destroyedViews; + static inline StorageUploadProbe* active = nullptr; +}; + +void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool failView) { + auto context = device.GetContext(); + context.hostImportAlignment = 0; + TextureDetiler detiler(context); + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + const auto guestBytes = static_cast(DescribeSurface(resource).guestBytes); + std::vector texels(guestBytes + 256, std::byte{0x55}); + std::vector keys(guestBytes / 256, std::byte{0}); + resource.baseAddress = (reinterpret_cast(texels.data()) + 255) & ~std::uintptr_t{255}; + resource.dccAddress = reinterpret_cast(keys.data()); + DccKeyProof proof; + Require(ProvedClearKeys(resource, guestBytes, proof) == DccKeys::Clear0000, "storage lifetime fixture has no DCC clear"); + StorageUploadProbe probe(context, failView); + Buffer readback(context, resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0x55, readback.Bytes().size()); + std::weak_ptr lifetime; + bool premature = false; + bool retained = true; + bool threw = false; + { + Recorder recorder(context); + recorder.Activate(); + try { + auto image = std::make_shared(context, detiler, resource, 0); + lifetime = image; + image.reset(); + retained = !lifetime.expired(); + } catch (const std::exception& error) { + if (!probe.FailedView() || std::string_view(error.what()).find("vkCreateImageView storage") == std::string_view::npos) throw; + threw = true; + } + premature = probe.Destroyed(); + Require(probe.Clears() == 1 && recorder.Recording(), "storage creation did not record its initial clear"); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {resource.width, resource.height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, probe.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.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); + recorder.Sync(); + } + previous.Activate(); + Require(threw == failView && probe.FailedView() == failView, "storage view failure injection did not take the requested path"); + Require(std::all_of(readback.Bytes().begin(), readback.Bytes().end(), [](std::byte value) { return value == std::byte{0}; }), "recorded storage clear did not reach every texel"); + Require(lifetime.expired() && probe.Released(), "storage upload resources outlived recorder teardown"); + Require(!premature && retained, failView ? "failed storage creation released an image still referenced by its recorded clear" : "storage creation did not retain its image through the recorded clear"); + std::cout << "Storage upload lifetime" << (failView ? " after view failure" : " after caller release") << ": passed\n"; +} + void refreshProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -4933,6 +5072,10 @@ int main(int argc, char** argv) { std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && (std::string_view(argv[1]) == "--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; + } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; From 747321d0bb88e6a1bb756e1847585f966d8eeb3e Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:48:00 -0400 Subject: [PATCH 174/277] fix(agc): retain storage textures before recording initial uploads --- .../Graphics/include/Texture.hpp | 3 +- .../Graphics/src/ShaderResources.cpp | 4 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 37 +++++++++++-------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 34 ++++++++--------- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9f4c7765a..a974a808f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -142,7 +142,7 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); // or it leaves the cache. Dispatches reusing the image meanwhile skip the round trip entirely. class StorageTexture : public std::enable_shared_from_this { public: - StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); + static std::shared_ptr Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); ~StorageTexture(); StorageTexture(const StorageTexture&) = delete; StorageTexture& operator=(const StorageTexture&) = delete; @@ -326,6 +326,7 @@ public: VkDeviceSize AllocationBytes() const { return memoryBytes; } private: + StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); // The regions of every array layer, or of the tracked layers `layers` selects. std::vector CopyRegions(const std::vector* layers = nullptr) const; // Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..eb7a84d87 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -837,7 +837,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; - if (disabled) return std::make_shared(context, *context.detiler, viewed, mip); + if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); const bool profile = LookupOutcomes::Profiled(); @@ -869,7 +869,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } else { if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; if (StorageTexture::FlushPending(resource.baseAddress, static_cast(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); - CachedStorageTexture entry{key, mip, std::make_shared(context, *context.detiler, resource, mip)}; + CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)}; static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7a21d9720..931e08771 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -866,9 +866,29 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { return proxy; } -StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { +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; + auto texture = std::shared_ptr(new StorageTexture(context, detiler, descriptor, mipLevel)); + uploadReason = "first"; + texture->upload(); + texture->defaultMip = mipLevel; + texture->view = texture->createView(mipLevel, false, texture->storageFormat); + { + auto& live = Live(); + std::lock_guard lock(live.mutex); + live.textures.push_back(texture.get()); + } + if (profile) { + auto& totals = Profile(); + totals.storageCreate += timer.lap(); + totals.storageBytes += texture->guestBytes; + if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); + } + return texture; +} + +StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { try { Require(!IsBlockCompressed(descriptor.format), "block-compressed textures cannot be storage images"); Require(mipLevel < descriptor.mipCount, "storage texture mip level is outside the texture"); @@ -923,21 +943,6 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); - uploadReason = "first"; - upload(); - defaultMip = mipLevel; - view = createView(mipLevel, false, storageFormat); - { - auto& live = Live(); - std::lock_guard lock(live.mutex); - live.textures.push_back(this); - } - if (profile) { - auto& totals = Profile(); - totals.storageCreate += timer.lap(); - totals.storageBytes += guestBytes; - if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); - } } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index f65914e97..61fb9030b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2201,7 +2201,7 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) DccKeyProof proof; Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0000, "0x00 keys are not a 0000 clear"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -2555,7 +2555,7 @@ void staleGenerationTests(const Device& device, Recorder& recorder) { resource.dccAddress = address + surfaceBytes; { Require(AgcDriver::GuestMemory::Watched(address, bytes), "(s) the block is not watched before the storage image takes its generation"); - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -2697,7 +2697,7 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche context.detiler = &detiler; resource.baseAddress = address; { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -2874,7 +2874,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { for (auto& value : texels) value = random(); const auto movesBefore = StorageTexture::SinglePassMoves(); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(image); results[mode].uploaded = readImage(*image); Buffer staging(context, linearBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -2898,7 +2898,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { recorder.Submit(); device.WaitQueue(); recorder.Sync(); - auto again = std::make_shared(context, detiler, resource, 0); + auto again = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(again); results[mode].reuploaded = readImage(*again); std::vector read(bytes); @@ -3041,7 +3041,7 @@ void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool f Recorder recorder(context); recorder.Activate(); try { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); lifetime = image; image.reset(); retained = !lifetime.expired(); @@ -3126,7 +3126,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { resource.dccAddress = address + surfaceBytes; const auto proved = [] { return StorageTexture::RefreshesProved(); }; { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -3171,7 +3171,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { expectProved("the proof was not taken again after a key store"); auto other = resource; other.dccAddress = 0; - auto pending = std::make_shared(context, detiler, other, 0); + auto pending = StorageTexture::Create(context, detiler, other, 0); recorder.Keep(pending); pending->MarkDirty(); expectFull("another image's pending results over the surface were answered from the proof"); @@ -3180,7 +3180,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { pending->MarkDirty(); expectFull("another image's pending results were answered from the proof"); Require(!AgcDriver::Graphics::PendingStorageOverlaps(address, surfaceBytes, image.get()), "the full refresh left another image's results pending"); - auto fresh = std::make_shared(context, detiler, resource, 0); + auto fresh = StorageTexture::Create(context, detiler, resource, 0); const auto before = proved(); fresh->Refresh(); Require(proved() == before, "a new image of the surface was answered from another image's proof"); @@ -3266,7 +3266,7 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { resource.dstSelW = 7; Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(c) unexpected surface size"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -3379,7 +3379,7 @@ void settledBaselineTests(const Device& device, Recorder& recorder) { if (pending) recorder.NotePendingWrite(address + 4096, 4096); const auto before = recorder.Submissions(); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(image); if (pending) Require(recorder.Submissions() == before, "(b) an upload over a pending recorded store submitted the open batch to read its generation baseline"); } @@ -3469,7 +3469,7 @@ void importWindowTests(const Device& device, Recorder& recorder) { Require(!HostImportCovers(base, address, bytes), "(w) the block is imported before the cache takes its generation"); const auto cached = CollectWritesUncached(address, surfaceBytes); Require(cached != 0 && UnchangedSince(address, surfaceBytes, cached), "(w) the cache's generation is not current before the import"); - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -4579,7 +4579,7 @@ void singleCubeTests(const Device& device, Recorder& recorder) { resource.baseAddress = reinterpret_cast(surface); const std::span snapshot(surface, static_cast(geometry.guestBytes)); Texture texture(context, detiler, resource, identity, snapshot); - auto storage = std::make_shared(context, detiler, resource, 0); + auto storage = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(storage); Texture storageView(context, storage, resource, identity); for (std::uint32_t face = 0; face < 6; ++face) { @@ -4613,7 +4613,7 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); for (std::uint32_t layer = 0; layer < 3; ++layer) std::memset(surface + geometry.GuestLayerOffset(layer), 0x20 * (layer + 1), static_cast(geometry.layerBytes)); resource.baseAddress = reinterpret_cast(surface); - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); SampleProgram program(context, recorder); expectRed(program.Red(image->FirstLayerView(0), VK_IMAGE_LAYOUT_GENERAL, 0.0f), 0x40 / 255.0f, "a first-layer view does not read the BASE_ARRAY layer"); @@ -4654,7 +4654,7 @@ void atomicViewTests(const Device& device, Recorder& recorder) { const auto geometry = DescribeSurface(resource); std::vector memory(static_cast(geometry.guestBytes) + 256); resource.baseAddress = (reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}; - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); if (format == 56u) { bool refused = false; @@ -4824,7 +4824,7 @@ void keysFillTests(const Device& device, Recorder& recorder) { resource.dccAddress = keysAddress; Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the key fill surface has an unexpected size"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -4937,7 +4937,7 @@ void denormalClearTests(const Device& device, Recorder& recorder) { Require(ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT) == keeps, "the denormal clear probe changed its answer"); Require(!ClearKeepsDenormals(context, VK_FORMAT_R32G32B32A32_SFLOAT), "a format without small floats probed as keeping denormals"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const auto holds = [&](std::uint32_t texel) { Buffer readback(context, side * side * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder.Commands(); From 9f567c227e03797dccb06a9547ea12915ef436e2 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:49:32 -0400 Subject: [PATCH 175/277] test(agc): report recorder test Vulkan device --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 61fb9030b..0d6b987ed 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -250,6 +250,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Vulkan device: " << properties.deviceName << '\n'; context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); From 804fcb41e3c971433e3a688592f18ff3b17ae34e Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:50:37 -0400 Subject: [PATCH 176/277] test(agc): keep storage probe callbacks out of buffer pools --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0d6b987ed..1cd9af26c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3032,7 +3032,7 @@ void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool f DccKeyProof proof; Require(ProvedClearKeys(resource, guestBytes, proof) == DccKeys::Clear0000, "storage lifetime fixture has no DCC clear"); StorageUploadProbe probe(context, failView); - Buffer readback(context, resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + Buffer readback(device.GetContext(), resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::memset(readback.Bytes().data(), 0x55, readback.Bytes().size()); std::weak_ptr lifetime; bool premature = false; From 34688d3a69c6ff59693d4bb3071ab40fd593c55a Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:51:54 +0200 Subject: [PATCH 177/277] test(exports): drop the PPSA07411 export test CONTRIBUTING asks for no tests that only check that a symbol is exported. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSsl/CMakeLists.txt | 9 ---- core/libs/tests/GuestPPSA07411Exports.cpp | 62 ----------------------- 2 files changed, 71 deletions(-) delete mode 100644 core/libs/tests/GuestPPSA07411Exports.cpp diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index aa3508b58..d66b9716e 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -12,13 +12,4 @@ if(BUILD_TESTING) set_tests_properties(guest_ssl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test_executable(guest_ppsa07411_exports_tests ${LIBS_INCLUDE_DIR}/tests/GuestPPSA07411Exports.cpp) - target_include_directories(guest_ppsa07411_exports_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ppsa07411_exports_tests PRIVATE libSceSsl libSceJson2 libSceNpSessionSignaling libSceConvertKeycode libc) - configure_windows_unwind(guest_ppsa07411_exports_tests) - add_test(NAME guest_ppsa07411_exports COMMAND guest_ppsa07411_exports_tests) - if(WIN32) - set_tests_properties(guest_ppsa07411_exports PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() endif() diff --git a/core/libs/tests/GuestPPSA07411Exports.cpp b/core/libs/tests/GuestPPSA07411Exports.cpp deleted file mode 100644 index 92acb9a86..000000000 --- a/core/libs/tests/GuestPPSA07411Exports.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI sceSslCreateConnection(); -int APS5_VABI sceSslDeleteConnection(); -int APS5_VABI sceSslEnableVerifyOption(); -int APS5_VABI sceSslConnect(); -int APS5_VABI sceSslRead(); -int APS5_VABI sceSslWrite(); -int APS5_VABI sceConvertKeycodeGetCharacter(); -int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(); -int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(); -int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(); -int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(); -int APS5_VABI _ZNK3sce4Json5ValuecvbEv(); -} - -namespace { - -struct Export { - const char* name; - int (APS5_VABI *function)(); -}; - -const Export exports[] = { - {"sceSslCreateConnection", sceSslCreateConnection}, - {"sceSslDeleteConnection", sceSslDeleteConnection}, - {"sceSslEnableVerifyOption", sceSslEnableVerifyOption}, - {"sceSslConnect", sceSslConnect}, - {"sceSslRead", sceSslRead}, - {"sceSslWrite", sceSslWrite}, - {"sceConvertKeycodeGetCharacter", sceConvertKeycodeGetCharacter}, - {"sceNpSessionSignalingGetGroupFromSessionId", sceNpSessionSignalingGetGroupFromSessionId}, - {"_ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_", _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_}, - {"_ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_", _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_}, - {"_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_", _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_}, - {"_ZNK3sce4Json5ValuecvbEv", _ZNK3sce4Json5ValuecvbEv}, -}; - -bool ReportsNotImplemented(const Export& entry) { - try { - entry.function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(entry.name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", entry.name, error.what()); - return false; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", entry.name); - return false; -} - -} - -int main() { - bool correct = true; - for (const auto& entry : exports) correct &= ReportsNotImplemented(entry); - return correct ? 0 : 1; -} From 0a6247532edd12388308e5d77b1a57dce5867f96 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 16:54:02 +0000 Subject: [PATCH 178/277] feat(relinker): trap exact SDK syscall stubs on request --- core/relinker/CMakeLists.txt | 2 + core/relinker/cli/include/Cli.hpp | 1 + core/relinker/cli/src/CliArgs.cpp | 6 ++- core/relinker/cli/tests/CliArgsTests.cpp | 10 +++- core/relinker/main.cpp | 2 +- .../relinker/analysis/SyscallScanner.hpp | 2 +- .../relinker/domain/ISyscallScanner.hpp | 3 +- .../relinker/src/analysis/SyscallScanner.cpp | 50 ++++++++++++++++--- .../relinker/src/guest/GuestModuleBuilder.cpp | 7 ++- .../src/pipeline/RelinkerPipeline.cpp | 24 ++++++--- .../relinker/tests/SyscallScannerTests.cpp | 38 ++++++++++++++ .../relinker/tests/test_guest_syscall_trap.py | 42 ++++++++++++++++ docs/user/USAGE.md | 3 ++ 13 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_syscall_trap.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..c92b6167a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -297,6 +297,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_desktop_entry_output.py $) add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) + add_test(NAME guest_syscall_trap COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_syscall_trap.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 6f7985c1c..7faa5353b 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -11,6 +11,7 @@ namespace Cli { struct Args { bool showHelp = false; bool skipSyscallCheck = false; + bool trapSdkSyscallStubs = false; bool skipSceModule = false; bool toIntel = false; bool toRosetta = false; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 0528e8419..eef10daad 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -8,7 +8,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--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] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } @@ -23,6 +23,8 @@ Args ParseArgs(int argc, char* argv[]) { args.showHelp = true; } else if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; + } else if (arg == "--trap-sdk-syscall-stubs") { + args.trapSdkSyscallStubs = true; } else if (arg == "--skip-sce-module") { args.skipSceModule = true; } else if (arg == "--sce-module-path") { @@ -80,6 +82,8 @@ Args ParseArgs(int argc, char* argv[]) { if (args.skipSceModule && sceModulePathSpecified) throw std::runtime_error("--sce-module-path conflicts with --skip-sce-module"); + if (args.skipSyscallCheck && args.trapSdkSyscallStubs) throw std::runtime_error("--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); + if (args.skipSceModule && !args.excludedSceModules.empty()) throw std::runtime_error("--exclude-sce-module conflicts with --skip-sce-module"); diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 98f3b2997..ec27e771a 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -52,7 +52,7 @@ void defaults() { const auto args = parse({"input.elf", "output.elf"}); require(args.inputPath == "input.elf", "input path was not captured"); require(args.outputPath == "output.elf", "output path was not captured"); - require(!args.skipSyscallCheck && !args.skipSceModule && !args.toIntel && !args.writeRegistry && + require(!args.skipSyscallCheck && !args.trapSdkSyscallStubs && !args.skipSceModule && !args.toIntel && !args.writeRegistry && !args.toWindows && !args.lazyBinding && !args.autorun && !args.windowsDiagnostics && !args.windowsGui, "a flag defaulted to true"); require(args.unusedFilterLevel == 0u, "unused filter level did not default to 0"); @@ -76,6 +76,13 @@ void booleanFlags() { require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "flags consumed the positional paths"); } +void trapSdkSyscallStubs() { + const auto args = parse({"--trap-sdk-syscall-stubs", "input.elf", "output.elf"}); + require(args.trapSdkSyscallStubs, "--trap-sdk-syscall-stubs was ignored"); + expectThrows({"--skip-syscall-check", "--trap-sdk-syscall-stubs", "input.elf", "output.elf"}, + "--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); +} + void positionalsInterleaveWithFlags() { const auto args = parse({"--windows", "input.elf", "--registry", "output.elf", "--autorun"}); require(args.toWindows && args.writeRegistry && args.autorun, "a flag after a positional was ignored"); @@ -189,6 +196,7 @@ int main() { try { defaults(); booleanFlags(); + trapSdkSyscallStubs(); positionalsInterleaveWithFlags(); singleDashIsPositional(); rpath(); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f1b4a5b64..fd1a700f0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -85,7 +85,7 @@ int Run(const int argc, char* argv[]) { } auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(); + const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs); const auto pipeline = std::make_shared( elfReader, diff --git a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp index 2e4551de7..29e14cf09 100644 --- a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp @@ -6,7 +6,7 @@ namespace Relinker { -std::unique_ptr MakeSyscallScanner(); +std::unique_ptr MakeSyscallScanner(bool trapSdkStubs = false); std::unique_ptr MakeNullSyscallScanner(); } diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 3986e965e..021938a31 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -2,6 +2,7 @@ #define RELINKER_DOMAIN_ISYSCALLSCANNER_HPP #include +#include #include namespace Relinker { @@ -10,7 +11,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual void ScanCodeSectionForSyscalls( + virtual std::vector ScanCodeSectionForSyscalls( const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 45fa513c8..a7e80bcf0 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -2,7 +2,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -19,6 +22,28 @@ static constexpr std::uint8_t SYSRET_BYTE0 = 0x0F; static constexpr std::uint8_t SYSRET_BYTE1 = 0x07; static constexpr std::size_t SYSCALL_NUMBER_LOOKBACK = 8; +static constexpr std::array SDK_GENERIC_SYSCALL_STUB{ + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, +}; + +static bool IsSdkSyscallStub(const std::vector& code, const std::size_t syscallOffset) { + constexpr std::array numberLoad{0x48, 0xC7, 0xC0}; + constexpr std::array argumentShuffle{0x49, 0x89, 0xCA}; + if (syscallOffset <= code.size() && syscallOffset >= SDK_GENERIC_SYSCALL_STUB.size() && code.size() - syscallOffset >= 3u) { + const auto begin = code.begin() + static_cast(syscallOffset - SDK_GENERIC_SYSCALL_STUB.size()); + if (std::equal(SDK_GENERIC_SYSCALL_STUB.begin(), SDK_GENERIC_SYSCALL_STUB.end(), begin) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3) + return true; + } + if (syscallOffset > code.size() || syscallOffset < 10u || code.size() - syscallOffset < 3u) + return false; + const auto begin = code.begin() + static_cast(syscallOffset - 10u); + return std::equal(numberLoad.begin(), numberLoad.end(), begin) + && std::equal(argumentShuffle.begin(), argumentShuffle.end(), begin + 7) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3; +} + enum class ForbiddenInstruction { None, Invocation, @@ -100,13 +125,18 @@ static std::optional ConstantSyscallNumber( class SyscallScanner : public ISyscallScanner { public: - void ScanCodeSectionForSyscalls( + explicit SyscallScanner(const bool trapSdkStubs) : _trapSdkStubs(trapSdkStubs) {} + + std::vector ScanCodeSectionForSyscalls( const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize) override; + +private: + bool _trapSdkStubs; }; -void SyscallScanner::ScanCodeSectionForSyscalls( +std::vector SyscallScanner::ScanCodeSectionForSyscalls( const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize) { @@ -115,6 +145,7 @@ void SyscallScanner::ScanCodeSectionForSyscalls( const auto scanner = Codegen::MakeInstructionScanner(); const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); + std::vector trapped; for (std::size_t index = 0; index < matches.size(); ++index) { const auto& match = matches[index]; const std::size_t start = static_cast(match.Offset - codeSectionOffset); @@ -127,6 +158,12 @@ void SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; + if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && IsSdkSyscallStub(codeSection, opcode)) { + trapped.push_back(opcode); + std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; + continue; + } + std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; @@ -136,19 +173,20 @@ void SyscallScanner::ScanCodeSectionForSyscalls( throw RelinkerException(msg.str(), match.Offset); } + return trapped; } -std::unique_ptr MakeSyscallScanner() { - return std::make_unique(); +std::unique_ptr MakeSyscallScanner(const bool trapSdkStubs) { + return std::make_unique(trapSdkStubs); } class NullSyscallScanner : public ISyscallScanner { public: - void ScanCodeSectionForSyscalls( + std::vector ScanCodeSectionForSyscalls( const std::vector&, FileByteOffset, FileByteOffset - ) override {} + ) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 689f4dafd..a3eee3ddc 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -342,7 +342,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& header : codeHeaders) { const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize)) { + if (offset > code.size() || code.size() - offset < 2u) + throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); + image.Bytes[header.Offset + offset] = 0x0F; + image.Bytes[header.Offset + offset + 1] = 0x0B; + } } const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); const auto destination = outputDirectory / relativeDirectory; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 5b28eb708..8ff30c80b 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Relinker { @@ -46,7 +47,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector textSection; VirtualAddress textVAddr = 0; - std::vector, VirtualAddress>> executableSegments; + std::vector, VirtualAddress, FileByteOffset>> executableSegments; VirtualAddress gotVAddr = 0; ByteCount gotSize = 0; @@ -58,7 +59,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf textSection = segment; textVAddr = ph.MappedAddress; } - executableSegments.emplace_back(std::move(segment), ph.MappedAddress); + executableSegments.emplace_back(std::move(segment), ph.MappedAddress, ph.Offset); } } } @@ -257,7 +258,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); - for (auto& [segment, segmentVAddr] : executableSegments) { + 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()) @@ -292,8 +294,16 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } } - for (const auto& [segment, segmentVAddr] : executableSegments) - _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size()); + std::vector syscallPatches; + for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (const auto offset : _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + if (offset > segment.size() || segment.size() - offset < 2u) + throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); + segment[offset] = 0x0F; + segment[offset + 1] = 0x0B; + syscallPatches.push_back({segmentFileOffset + offset, {0x0F, 0x0B}}); + } + } _validationPolicy->ValidateSyscallAbsence(); @@ -416,7 +426,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr] : executableSegments) { + for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + static_cast(segmentFileOffset); for (auto& entry : entries) { auto segmentSites = _callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); @@ -424,6 +435,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } } + patches.insert(patches.end(), std::make_move_iterator(syscallPatches.begin()), std::make_move_iterator(syscallPatches.end())); return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr, std::move(patches)}; } diff --git a/core/relinker/relinker/tests/SyscallScannerTests.cpp b/core/relinker/relinker/tests/SyscallScannerTests.cpp index 3b7846a8e..65901a6f3 100644 --- a/core/relinker/relinker/tests/SyscallScannerTests.cpp +++ b/core/relinker/relinker/tests/SyscallScannerTests.cpp @@ -70,6 +70,12 @@ const Bytes kPrefixedClobberedStub = { 0xC3, }; +const Bytes kSdkGenericStub = { + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, + 0x0F, 0x05, 0xC3, +}; + std::string RunScanner(const std::string& binary, const Bytes& code) { const TempDirectory directory; const auto input = directory.Path() / "input.elf"; @@ -96,6 +102,35 @@ void RequireNoNumber(const std::string& binary, const Bytes& code, const std::st "Relinker reported a syscall number for the " + description + ":\n" + output); } +void RequireSdkStubsAreTrapped(const std::string& binary) { + Bytes code = kConstantStub; + const auto numberedOffset = code.size() - 3u; + code.insert(code.end(), kSdkGenericStub.begin(), kSdkGenericStub.end()); + const auto genericOffset = kConstantStub.size() + kSdkGenericStub.size() - 3u; + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(code)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker did not trap known SDK syscall stubs:\n" + run.Output); + const auto relinked = ReadFile(output); + require(Read(relinked, kCodeOffset + numberedOffset) == 0x0B0Fu, + "Relinker did not replace the numbered SDK syscall with UD2"); + require(Read(relinked, kCodeOffset + genericOffset) == 0x0B0Fu, + "Relinker did not replace the generic SDK syscall with UD2"); +} + +void RequireUnknownSyscallsStillFail(const std::string& binary) { + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kRegisterStub)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker accepted an unknown syscall in SDK-stub trap mode:\n" + run.Output); + require(run.Output.find("Forbidden syscall instruction at code offset 0x") != std::string::npos, + "Relinker did not report an unknown syscall in SDK-stub trap mode:\n" + run.Output); +} + } int main(const int argc, char** argv) { @@ -115,6 +150,9 @@ int main(const int argc, char** argv) { RequireNumber(binary, kPrefixedConstantStub, "syscall number 9", "prefixed constant stub"); RequireNoNumber(binary, kPrefixedClobberedStub, "prefixed stub whose intervening instruction writes rax"); + RequireSdkStubsAreTrapped(binary); + RequireUnknownSyscallsStillFail(binary); + std::cout << "syscall scanner tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/test_guest_syscall_trap.py b/core/relinker/relinker/tests/test_guest_syscall_trap.py new file mode 100644 index 000000000..8a94d30f3 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_syscall_trap.py @@ -0,0 +1,42 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture + + +GENERIC_STUB = bytes.fromhex( + "48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 " + "4c 8b 4c 24 08 0f 05 c3") +NUMBERED_STUB = bytes.fromhex("48 c7 c0 09 00 00 00 49 89 ca 0f 05 c3") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-guest-syscall-trap-") as directory: + work = Path(directory) + modules = work / "sce_module" + modules.mkdir() + module = bytearray(guest_fixture(b"\x90" * 6)) + module[0x400:0x400 + len(GENERIC_STUB)] = GENERIC_STUB + module[0x440:0x440 + len(NUMBERED_STUB)] = NUMBERED_STUB + (modules / "sample.prx").write_bytes(module) + source = work / "input.elf" + source.write_bytes(main_fixture()) + output = work / "output.elf" + result = subprocess.run( + [str(relinker), "--trap-sdk-syscall-stubs", str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + + converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" + image = converted.read_bytes() + loads = elf_loads(image) + assert elf_bytes_at(image, loads, 0x1017, 3) == b"\x0f\x0b\xc3" + assert elf_bytes_at(image, loads, 0x104a, 3) == b"\x0f\x0b\xc3" + + +if __name__ == "__main__": + main() diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..661305c2f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -60,12 +60,15 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | +| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, and `--lazy-binding` flags are deprecated. If the application runs with these flags enabled, it will be extremely unstable and unsuitable for general use. These flags are only for debugging. +`--trap-sdk-syscall-stubs` is also for debugging. It preserves a runtime failure if a syscall wrapper is called; it does not implement FreeBSD syscalls or make these applications generally safe to run. + ## Runtime layout Paths are relative to the output executable: From 7c2eba0a6d6439a9fbea7b00e6b29c0c7e6396ff Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 16:57:06 +0000 Subject: [PATCH 179/277] fix(libkernel): validate APR command payload sizes --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 11 +++++--- core/libs/tests/GuestAmpr.cpp | 37 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8aa37a332..756ff4fa8 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -242,6 +242,9 @@ bool _waitSatisfied(std::uint32_t compare, std::uint64_t value, std::uint64_t re template TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { + Apr::CommandHeader header; + std::memcpy(&header, buffer.base + cursor, sizeof(header)); + if (header.bytes < sizeof(TCommand)) throw std::runtime_error("APR: truncated command payload (opcode " + std::to_string(static_cast(header.opcode)) + ", has " + std::to_string(header.bytes) + ", needs " + std::to_string(sizeof(TCommand)) + ")"); TCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); return command; @@ -532,14 +535,15 @@ void _execute(const Apr::CommandBufferObject& buffer) { std::memcpy(&header, buffer.base + cursor, sizeof(header)); if ((static_cast(header.opcode) & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { if (sizeof(Apr::CompactReadFileGatherCommand) > buffer.offset - cursor) throw std::runtime_error("APR: truncated compact gather command"); - const auto command = _read(buffer, cursor); + Apr::CompactReadFileGatherCommand command; + std::memcpy(&command, buffer.base + cursor, sizeof(command)); const auto offset = static_cast(command.opcodeAndOffsetHigh >> 8u) << 32u | command.offsetLow; if (offset >= 0x10000000000ull) throw std::runtime_error("APR: malformed compact gather offset"); _readResolved(Apr::Opcode::ReadFileGather, Apr::ReadFileCommand{{}, 0, 0, 0, static_cast(command.sizeMinusOne) + 1u, offset}, read); cursor += sizeof(command); continue; } - if (header.bytes < sizeof(header) || cursor + header.bytes > buffer.offset) throw std::runtime_error("APR: malformed command"); + if (header.bytes < sizeof(header) || header.bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); switch (header.opcode) { case Apr::Opcode::Nop: case Apr::Opcode::PushMarker: @@ -585,8 +589,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { case Apr::Opcode::MapEnd: break; case Apr::Opcode::WriteAddress: { - Apr::WriteAddressCommand command; - std::memcpy(&command, buffer.base + cursor, sizeof(command)); + const auto command = _read(buffer, cursor); _writeAddress(command); break; } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 43b223fac..621bfa181 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -579,6 +581,40 @@ void TestResolveFailures() { std::remove(paths[0]); } +void TestMalformedCommandSizes() { + const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + std::array memory{}; + const Apr::CommandHeader header{opcode, recordBytes}; + std::memcpy(memory.data(), &header, sizeof(header)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, bufferBytes, 1, bufferBytes, memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with(expected); + } + Require(rejected); + }; + + requireFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::Nop, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + + std::array memory{}; + const Apr::CommandHeader first{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; + const Apr::CommandHeader overflowing{Apr::Opcode::Nop, std::numeric_limits::max()}; + std::memcpy(memory.data(), &first, sizeof(first)); + std::memcpy(memory.data() + sizeof(first), &overflowing, sizeof(overflowing)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 2, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("APR: malformed command"); + } + Require(rejected); +} + void TestGatherScatter() { const char* path = "ampr_gather_scatter.bin"; { @@ -1054,6 +1090,7 @@ int main() { TestConstructed(); TestZeroFilledBuffer(); TestResolveFailures(); + TestMalformedCommandSizes(); TestGatherScatter(); TestAmmRangesAboveHintedMaps(); TestAmm(); From 6200b8c7b532d05e74c90ef6d48cc0ebcc7d35f4 Mon Sep 17 00:00:00 2001 From: David-cmrg <202826622+David-cmrg@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:01:47 +0200 Subject: [PATCH 180/277] test(agc): split the gated dispatch producer spin into short dispatches --- .../tests/execution/GatedDispatchCapture.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp index fe73e2b3a..8fe09da45 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp @@ -23,6 +23,7 @@ alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, constexpr std::uint32_t StaleValue = 0x57a1e000u; constexpr std::uint32_t FreshValue = 0xf7e54000u; +constexpr std::uint32_t SpinIterationsPerDispatch = 1u << 20u; struct Slot { alignas(256) std::array stale{}; @@ -81,14 +82,14 @@ void Submit(std::uint32_t queue, std::vector& words) { enum class Outcome { Fresh, Stale, Inconclusive }; -Outcome Run(Slot& slot, std::uint32_t iterations) { +Outcome Run(Slot& slot, std::uint32_t spinDispatches) { slot.stale[0] = StaleValue; slot.fresh[0] = FreshValue; slot.table = {Low(slot.stale.data()), High(slot.stale.data())}; slot.outputDescriptor = Descriptor(slot.output.data(), sizeof(slot.output)); slot.spinDescriptor = Descriptor(slot.spinOutput.data(), sizeof(slot.spinOutput)); std::vector producer; - Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), iterations}); + for (std::uint32_t index = 0; index < spinDispatches; ++index) Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterationsPerDispatch}); WriteData(producer, &slot.label, 1); std::vector consumer; WaitEqual(consumer, &slot.label, 1); @@ -118,16 +119,16 @@ int main() { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); - std::uint32_t iterations = 1u << 26u; + std::uint32_t spinDispatches = (1u << 26u) / SpinIterationsPerDispatch; for (auto& slot : Slots) { - const auto outcome = Run(slot, iterations); + const auto outcome = Run(slot, spinDispatches); Require(outcome != Outcome::Stale, "a dispatch behind a cross-queue WAIT_REG_MEM read its table before the producer's label reached memory, so it missed the table the CPU filled after submitting"); if (outcome == Outcome::Fresh) { - std::cout << "gated dispatch capture test passed (" << iterations << " producer iterations)\n"; + std::cout << "gated dispatch capture test passed (" << spinDispatches << " producer dispatches of " << SpinIterationsPerDispatch << " iterations)\n"; AgcDriverShutdown_nid_postfix(); return 0; } - iterations *= 2u; + spinDispatches *= 2u; } std::cout << "the producer's label always reached memory before the table was filled: gated dispatch capture not tested\n"; AgcDriverShutdown_nid_postfix(); From 88e5a2e40730aae82de11d96442b38ebf3c2f1f3 Mon Sep 17 00:00:00 2001 From: winstrex <79346005+winstrex@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:48:34 -0300 Subject: [PATCH 181/277] test(agc): split agc_driver_flat_apertures by shader instead of by row Rows that run the same kernel at the same wave size compile the same pipeline, so each part now takes whole groups of them, and the test is split into fourteen parts. Each shader is compiled once per CI run instead of in up to three parts at once. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++-- .../prx/libSceAgcDriver/tests/execution/FlatApertures.cpp | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..2685b1937 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1035,8 +1035,8 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flatAperturesPart RANGE 5) - add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/6) + foreach(flatAperturesPart RANGE 13) + add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/14) set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp index c404a90f9..72f895af1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -497,9 +498,10 @@ int main(int argc, char** argv) { const bool int64Atomics = TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics); GuestBlock guest; std::size_t checked = 0; - const auto rows = Rows(); - for (std::size_t index = part; index < rows.size(); index += parts) { - const Row& row = rows[index]; + std::map, std::size_t> shaders; + for (const Row& row : Rows()) { + const auto shader = shaders.emplace(std::pair{Kernel(row), row.waveSize}, shaders.size()).first->second; + if (shader % parts != part) continue; if (row.waveSize == 64u && !wave64) continue; if (row.atomic && row.width == 64u && !int64Atomics) continue; Run(*device, row, guest); From 33c6e825ab3ad01a8750edf9e51331bac26be326 Mon Sep 17 00:00:00 2001 From: Semir Kabir <66262518+semirkabir@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:06:27 -0700 Subject: [PATCH 182/277] feat(libSceUsbd): declare the device and handle calls for the no-device path --- core/libs/prx/libSceUsbd/Export.cpp | 135 ++++++++++++++++------------ core/libs/tests/GuestUsbd.cpp | 72 +++++++++++++-- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 143 insertions(+), 66 deletions(-) diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index a67889f5c..d0be202ca 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -50,6 +50,14 @@ static_assert(offsetof(UsbdTransfer, timeout) == 12 && offsetof(UsbdTransfer, ca void* g_emptyDeviceList[1] = {nullptr}; +std::string AbsentDevice(const char* function) { + return std::string(function) + ": the device list is always empty, so the device was not returned by sceUsbdGetDeviceList"; +} + +std::string UnopenedHandle(const char* function) { + return std::string(function) + ": no device can be opened, so the handle was not returned by sceUsbdOpen"; +} + } extern "C" { @@ -116,99 +124,112 @@ void APS5_VABI sceUsbdFreeConfigDescriptor(void* config) { if (config != nullptr) throw std::runtime_error(std::string(__func__) + ": descriptor was not returned by this library"); } -int APS5_VABI sceUsbdAttachKernelDriver() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdCheckConnected() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdClaimInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdClose(void* deviceHandle) { + if (deviceHandle != nullptr) throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdControlTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout) { + (void)requestType; + (void)request; + (void)value; + (void)index; + (void)timeout; + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdGetActiveConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config) { + (void)config; + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetBusNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device) { + if (device == nullptr) return 0; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config) { + (void)configIndex; + if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetDeviceAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device) { + if (device == nullptr) return 0; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetDeviceDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor) { + if (device == nullptr || descriptor == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetStringDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length) { + (void)descriptorIndex; + (void)languageId; + if (deviceHandle == nullptr || data == nullptr || length < 1) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdKernelDriverActive() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle) { + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdRefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceUsbdRefDevice(void* device) { + if (device == nullptr) return nullptr; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdReleaseInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdResetDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdSetConfiguration() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration) { + (void)configuration; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdSubmitTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSubmitTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr || transfer->deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdUnrefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdUnrefDevice(void* device) { + if (device != nullptr) throw std::runtime_error(AbsentDevice(__func__)); } } diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 086487a48..e91e1da8c 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -21,7 +21,25 @@ void APS5_VABI sceUsbdExit(); std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list); void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices); std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout); -int APS5_VABI sceUsbdOpen(); +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle); +void APS5_VABI sceUsbdClose(void* deviceHandle); +void* APS5_VABI sceUsbdRefDevice(void* device); +void APS5_VABI sceUsbdUnrefDevice(void* device); +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device); +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device); +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor); +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config); +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config); +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration); +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout); +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length); +std::int32_t APS5_VABI sceUsbdSubmitTransfer(void* transfer); void* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets); void APS5_VABI sceUsbdFreeTransfer(void* transfer); void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, void* callback, void* userData, std::uint32_t timeout); @@ -41,6 +59,16 @@ void Require(bool condition, const char* message) { } } +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + } int main() { @@ -57,13 +85,37 @@ int main() { const auto start = std::chrono::steady_clock::now(); Require(sceUsbdHandleEventsTimeout(&timeout) == 0, "event handling failed"); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(45), "event handling returned before the timeout"); - bool threw = false; - try { - sceUsbdOpen(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "opening a device did not throw"); + int foreign = 0; + void* handle = &foreign; + Require(sceUsbdOpen(&foreign, nullptr) == invalidArgument, "null handle output accepted"); + Require(Throws([&] { sceUsbdOpen(nullptr, &handle); }), "opening a null device did not throw"); + Require(Throws([&] { sceUsbdOpen(&foreign, &handle); }), "opening a device that was never listed did not throw"); + sceUsbdClose(nullptr); + Require(Throws([&] { sceUsbdClose(&foreign); }), "closing a handle that was never opened did not throw"); + Require(sceUsbdRefDevice(nullptr) == nullptr, "null device was referenced"); + sceUsbdUnrefDevice(nullptr); + Require(Throws([&] { sceUsbdRefDevice(&foreign); }), "referencing a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdUnrefDevice(&foreign); }), "unreferencing a device that was never listed did not throw"); + Require(sceUsbdGetBusNumber(nullptr) == 0 && sceUsbdGetDeviceAddress(nullptr) == 0, "null device reported a bus or address"); + Require(Throws([&] { sceUsbdGetBusNumber(&foreign); }), "bus number of a device that was never listed did not throw"); + std::uint8_t descriptor[18] = {}; + void* config = &foreign; + Require(sceUsbdGetDeviceDescriptor(nullptr, descriptor) == invalidArgument, "null device descriptor accepted"); + Require(sceUsbdGetDeviceDescriptor(&foreign, nullptr) == invalidArgument, "null descriptor output accepted"); + Require(sceUsbdGetConfigDescriptor(nullptr, 0, &config) == invalidArgument, "null config device accepted"); + Require(sceUsbdGetConfigDescriptor(&foreign, 0, nullptr) == invalidArgument, "null config output accepted"); + Require(Throws([&] { sceUsbdGetConfigDescriptor(&foreign, 0, &config); }), "config of a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdGetActiveConfigDescriptor(nullptr, &config); }), "active config of a null device did not throw"); + Require(sceUsbdClaimInterface(nullptr, 0) == invalidArgument && sceUsbdReleaseInterface(nullptr, 0) == invalidArgument, "null handle interface call accepted"); + Require(sceUsbdSetConfiguration(nullptr, 1) == invalidArgument && sceUsbdResetDevice(nullptr) == invalidArgument, "null handle configuration or reset accepted"); + Require(sceUsbdCheckConnected(nullptr) == invalidArgument, "null handle connection check accepted"); + Require(sceUsbdKernelDriverActive(nullptr, 0) == invalidArgument && sceUsbdAttachKernelDriver(nullptr, 0) == invalidArgument, "null handle kernel driver call accepted"); + Require(sceUsbdControlTransfer(nullptr, 0x80, 6, 0x100, 0, descriptor, 18, 1000) == invalidArgument, "null handle control transfer accepted"); + Require(sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, nullptr, 18, 1000) == invalidArgument, "control transfer without data accepted"); + Require(Throws([&] { sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, descriptor, 18, 1000); }), "control transfer on a handle that was never opened did not throw"); + Require(sceUsbdGetStringDescriptor(nullptr, 1, 0x409, descriptor, 18) == invalidArgument, "null handle string descriptor accepted"); + Require(sceUsbdGetStringDescriptor(&foreign, 1, 0x409, descriptor, 0) == invalidArgument, "empty string descriptor buffer accepted"); + Require(Throws([&] { sceUsbdClaimInterface(&foreign, 0); }), "claiming on a handle that was never opened did not throw"); auto* transfer = static_cast(sceUsbdAllocTransfer(2)); Require(transfer != nullptr, "transfer allocation failed"); Require(*reinterpret_cast(transfer + 56) == 2, "iso packet count not stored"); @@ -75,6 +127,10 @@ int main() { Require(*reinterpret_cast(transfer + 12) == 250 && *reinterpret_cast(transfer + 20) == 8, "timeout or length not filled"); Require(*reinterpret_cast(transfer + 40) == &user && *reinterpret_cast(transfer + 48) == buffer, "user data or buffer not filled"); Require(sceUsbdCancelTransfer(transfer) == static_cast(0x80240005), "cancelling an unsubmitted transfer did not report not found"); + Require(sceUsbdSubmitTransfer(nullptr) == invalidArgument, "null transfer submitted"); + Require(sceUsbdSubmitTransfer(transfer) == invalidArgument, "transfer without a handle submitted"); + sceUsbdFillInterruptTransfer(transfer, &foreign, 0x81, buffer, 8, reinterpret_cast(&Require), &user, 250); + Require(Throws([&] { sceUsbdSubmitTransfer(transfer); }), "submitting on a handle that was never opened did not throw"); sceUsbdFreeTransfer(transfer); sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..650039d4a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -25,7 +25,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported and device queries return no entries - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The layout and error codes are unverified on PS5 +- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The device and handle calls take the argument lists of FreeBSD 11.4 libusb ([libusb.h](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h)) and return `0x80240002` for the null arguments it rejects with `LIBUSB_ERROR_INVALID_PARAM` ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)); a null device or handle in the calls it treats as null-safe returns 0, null or nothing, as there. `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which libusb dereferences. Any other device or handle was never returned by this library, so those calls throw. The layout, the argument lists and the error codes are unverified on PS5 - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [libSceBluetoothHid](../../core/libs/prx/libSceBluetoothHid/Export.cpp) implements only the no-device path: initialization and callback and device registration succeed, the callback is never called because no device connects, and calls that need a connected device throw From f854e52b26b1d92c1e4d261415946f7a6cf37e98 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:06:38 +0200 Subject: [PATCH 183/277] docs: place the PPSA07411 TechnicalDebt entry away from the end of the file Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 63faecf90..9e47a8fa2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -591,10 +591,10 @@ Throughout the project, every function at every stage either **does exactly what - [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 throws, since guest signals are not delivered to a chosen thread - [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. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, 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) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [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. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, 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) From 28320acf2c07319d88d61551394576d2b1a6f29f Mon Sep 17 00:00:00 2001 From: gookie <36005381+gookie-dev@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:59:58 +0200 Subject: [PATCH 184/277] perf(libkernel): read a loaded image's unwind tables once in sceKernelGetModuleInfoFromAddr sceKernelGetModuleInfoFromAddr found the size of the image's .eh_frame on every call by walking all of its CIE and FDE records, checking each against every PT_LOAD header. The guest libc's unwinder calls it for each frame it unwinds, so a title that unwinds often walked its whole .eh_frame hundreds of times per second. The tables read from a .eh_frame_hdr are now kept per header address and reused while the image stays loaded. The cache is dropped whenever dl_iterate_phdr reports that an object was unloaded (dlpi_subs), so an address reused by another image is read again. guest_module_info queries a module twice and expects the same tables, then unloads it, loads it again and expects tables of the same size at the same offset in the new image. PPSA16827 (Harmony's Odyssey), title screen, over 500 calls per second counted under gdb, which slows the title: the process used 9269 CPU samples per 10 s, 53.7% of them in this walk, and 3306 with this change, 3.2% in the lookup. The title still runs at 27 FPS; its frame rate is limited elsewhere. --- .../prx/libkernel/Module/src/ModuleInfo.cpp | 23 +++++++++++++++++-- core/libs/tests/GuestModuleInfo.cpp | 11 +++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 66dc83653..b83f9a11f 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -22,7 +22,9 @@ #else #include #include +#include #include +#include #endif extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native); @@ -81,6 +83,24 @@ bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t siz return false; } +struct FrameTableCache { + std::mutex mutex; + unsigned long long unloads = 0; + std::unordered_map tables; +}; + +EhFrame::Tables FrameTables(const dl_phdr_info& image, std::uintptr_t header) { + static auto& cache = *new FrameTableCache(); + std::lock_guard lock(cache.mutex); + if (cache.unloads != image.dlpi_subs) { + cache.tables.clear(); + cache.unloads = image.dlpi_subs; + } + if (const auto found = cache.tables.find(header); found != cache.tables.end()) return found->second; + const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; + return cache.tables.emplace(header, EhFrame::ReadTables(contains, header, Caller)).first->second; +} + std::string ImageName(const dl_phdr_info& image) { std::string path = image.dlpi_name ? image.dlpi_name : ""; if (path.empty()) { @@ -111,8 +131,7 @@ void Fill(const dl_phdr_info& image, TInfo& info) { info.tls_size = ToU32(header.p_memsz, "TLS size"); info.tls_align = ToU32(header.p_align, "TLS alignment"); } else if (header.p_type == PT_GNU_EH_FRAME) { - const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; - const auto tables = EhFrame::ReadTables(contains, address, Caller); + const auto tables = FrameTables(image, address); info.eh_frame_hdr_addr = address; info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); info.eh_frame_addr = tables.frames; diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index b63e20a04..40eacdf81 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -114,5 +114,16 @@ int main(int argc, char** argv) { ModuleInfoEx unset{}; unset.st_size = 0xf0000; Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &unset) == 0 && unset.st_size == sizeof(ModuleInfoEx)); + const auto again = Query(add, 0); + Require(again.eh_frame_hdr_addr == info.eh_frame_hdr_addr && again.eh_frame_hdr_size == info.eh_frame_hdr_size); + Require(again.eh_frame_addr == info.eh_frame_addr && again.eh_frame_size == info.eh_frame_size); Require(dlclose_nid_postfix(module) == 0); + void* reloaded = dlopen_nid_postfix(argv[1], 2); + Require(reloaded != nullptr); + const void* reloadedAdd = dlsym_nid_postfix(reloaded, "GuestModuleAdd"); + Require(reloadedAdd != nullptr); + const auto fresh = Query(reloadedAdd, 0); + Require(fresh.eh_frame_size == info.eh_frame_size && fresh.eh_frame_hdr_size == info.eh_frame_hdr_size); + Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + Require(dlclose_nid_postfix(reloaded) == 0); } From 6693cd630106f1e7e81fb23db5f837a2a4123050 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 17:09:43 +0000 Subject: [PATCH 185/277] fix(agc): scope idle writeback and test queue worker --- .../src/Driver/Queues/QueueWorker.cpp | 45 ++++++++++--- .../tests/execution/StoreIdleResults.cpp | 64 +++++++++---------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 1f72d6255..d568f7b81 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -80,6 +81,24 @@ void Driver::run(std::uint32_t id) noexcept { try { for (;;) { submission = Submission{}; + struct Running { + std::atomic& count; + std::atomic& idleEpoch; + std::condition_variable& changed; + bool active = false; + Running(std::atomic& count, std::atomic& idleEpoch, std::condition_variable& changed) + : count(count), idleEpoch(idleEpoch), changed(changed) {} + void Start() { + count.fetch_add(1, std::memory_order_acq_rel); + active = true; + } + ~Running() { + if (active && count.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } + } running{runningWorkers, idleEpoch, changed}; static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; { PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async()); @@ -91,6 +110,10 @@ void Driver::run(std::uint32_t id) noexcept { workerQueued() = &worker.queued; if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id); const auto ready = [&] { return failure || stopping || !pending.empty(); }; + const auto allQueuesIdle = [&] { + return runningWorkers.load(std::memory_order_acquire) == 0 + && std::all_of(workers.begin(), workers.end(), [](const auto& item) { return item.second.pending.empty(); }); + }; const auto poll = [&](const auto& done) { #ifdef _WIN32 lock.unlock(); @@ -104,13 +127,21 @@ void Driver::run(std::uint32_t id) noexcept { while (!ready()) { if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) { + const auto idleEpochAtCheck = idleEpoch.load(std::memory_order_acquire); + const bool storeIdleResults = id == 0 && allQueuesIdle() && !completionsPending(); lock.unlock(); - if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(id == 0); + if (storeIdleResults) { + if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(); + } lock.lock(); if (ready()) break; + if (id == 0 && allQueuesIdle() && !completionsPending() + && (!storeIdleResults || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); - changed.wait(lock, ready); + const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); + changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); }); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); + if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue; break; } if (poll(ready)) break; @@ -137,16 +168,10 @@ void Driver::run(std::uint32_t id) noexcept { if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now(); timing.Mark("dequeue_order"); worker.queued.fetch_sub(1, std::memory_order_acq_rel); + running.Start(); if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - submission.enqueuedAt).count()); } - { - struct Running { - std::atomic& count; - explicit Running(std::atomic& count) : count(count) { count.fetch_add(1, std::memory_order_acq_rel); } - ~Running() { count.fetch_sub(1, std::memory_order_acq_rel); } - } running{runningWorkers}; - execute(submission); - } + execute(submission); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast(submission.serial), id); const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp index 65bcb0d52..cec60574e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp @@ -1,11 +1,7 @@ -#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.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/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" -#include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include #include @@ -13,16 +9,12 @@ #include #include #include -#include #include #include namespace { using AgcDriver::Graphics::Require; -using AgcDriver::Graphics::StorageTexture; -using ShaderRecompiler::ShaderStage; - constexpr std::uint32_t Threads = 8; constexpr std::uint32_t Side = 256; constexpr std::uint32_t Format32UInt = 20; @@ -53,31 +45,42 @@ std::array TextureDescriptor(const std::uint32_t* texels) { }; } -void Store(AgcDriver::VulkanDevice& device, const std::uint32_t* texels) { +std::vector Commands(const std::uint32_t* texels) { std::vector userData(16, 0u); const auto texture = TextureDescriptor(texels); std::copy(texture.begin(), texture.end(), userData.begin() + 8); - 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} + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); }; - request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); - device.WaitIdle(); + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(StoreCode.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + return words; +} + +void Store(const std::uint32_t* texels) { + auto words = Commands(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); } } int main() { try { - const auto device = OpenVulkanTestDevice(); - if (!device) return VulkanTestSkipped; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } std::vector storage((BlockBytes + 0x10000u) / 4u); auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); std::fill(texels, texels + BlockBytes / 4u, Untouched); @@ -85,7 +88,6 @@ int main() { GuestAllocations::Mutation mutation; mutation.Add(texels, BlockBytes, true, true, true); } - const auto surface = AddressOf(texels); const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; const volatile std::uint32_t* cpu = texels; const auto stored = [&] { @@ -95,18 +97,12 @@ int main() { return false; }; - Store(*device, texels); - Require(StorageTexture::FindPending(surface, bytes) != nullptr, "the store left no pending image"); - Require(!stored(), "the pending result reached guest memory before the idle store"); - - device->StoreIdleResults(); - Require(StorageTexture::FindPending(surface, bytes) == nullptr, "the idle store left the image pending"); + Store(texels); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); - Require(stored(), "the idle store did not bring the result to guest memory without a further submission"); + Require(stored(), "the queue-idle store did not bring the result to guest memory without a direct flush or further submission"); - device->StoreIdleResults(); - device->WaitIdle(); + AgcDriverShutdown_nid_postfix(); { GuestAllocations::Mutation mutation; mutation.Remove(texels); From 6f527fa956e5872db17add742f89d595854a9438 Mon Sep 17 00:00:00 2001 From: object05 Date: Sun, 11 Oct 2026 19:07:28 +0200 Subject: [PATCH 186/277] test(shader): accept the split fma sum in F32DenormalFlush agc_driver_f32_denormal_flush fails on RADV RENOIR (Mesa 26.2.4): lane 20 no float mode v_fma_f32 v14, v4, v5, v6 is 0x00800000, expected 0x00ffffff The driver lowers Fma to a multiply and an add: the compiled shader has v_mad_f32 and no v_fma_f32. The product 0x1fffffff * 0x1fffffff is below 2^-126 and is flushed before 0x00800000 is added. Lane 20 is the only row with a tiny product and a non-zero addend. v_fma_f32 and v_fmac_f32 fail on it in all three float modes; every other check passes. When both return that sum, the two values are not checked. FmaRounding already leaves out its fused forms on a device that splits fma. --- .../libSceAgcDriver/tests/execution/F32DenormalFlush.cpp | 8 ++++++++ docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index b089628a9..374eaa63a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -23,6 +23,8 @@ constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 19; constexpr std::uint32_t TinyResults = 16; +constexpr std::uint32_t TinyProductLane = 20; +constexpr std::uint32_t SplitTinyProductSum = 0x00800000u; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -411,9 +413,15 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional= 15u; if (floatResult && IsNan(Expected[tid][i])) { Require(IsNan(out[i]), std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..c038174cb 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -472,7 +472,7 @@ Throughout the project, every function at every stage either **does exactly what - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. +- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [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 - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. From 329b9d63bb5b7cb271d09f21e166ae070e195c52 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:50:02 +0200 Subject: [PATCH 187/277] fix(relinker): normalize additional module directory paths --- .../relinker/src/guest/GuestModuleBuilder.cpp | 7 +-- .../relinker/tests/test_guest_module_dir.py | 44 ++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 689f4dafd..7ce382aa7 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -70,10 +70,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (hasPrx) directories.push_back(prx); std::vector runtimeDirectories; for (const auto& name : moduleDirectories) { - const auto relative = Io::NativePath(name); - if (relative.empty() || relative.is_absolute() || relative.lexically_normal().generic_string().starts_with("..")) + const auto relative = Io::NativePath(name).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name); - const auto directory = (root / relative).lexically_normal(); + auto directory = (root / relative).lexically_normal(); + if (!directory.has_filename()) directory = directory.parent_path(); if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); directories.push_back(directory); diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 97f3c852c..4ab73d8af 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -39,13 +39,13 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-module-dir-") as directory: work = Path(directory) - def convert(case, windows, options, executable=None): + def convert(case, windows, options, executable=None, cwd=None): (case / "sce_module").mkdir(parents=True, exist_ok=True) source = case / "input.elf" source.write_bytes(main_fixture() if executable is None else executable) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), *options, str(source), str(output)], - capture_output=True, text=True, timeout=30) + cwd=cwd, capture_output=True, text=True, timeout=30) return result, output for windows in (False, True): @@ -101,6 +101,46 @@ def main(): result, output = convert(case, windows, ["--skip-sce-module", "--module-dir", "Media/Plugins"]) assert result.returncode != 0 and "conflicts with --skip-sce-module" in result.stderr, result.stderr + + for index, (name, relative) in enumerate((("..plugins", "..plugins"), ("Media/../..plugins", "..plugins"), + ("./..plugins/.", "..plugins"), ("Media/../Media/Plugins", "Media/Plugins"), + ("Media/Plugins/", "Media/Plugins"))): + case = work / f"{windows}-relative-{index}" + (case / "Media" / "Plugins").mkdir(parents=True) + (case / "..plugins").mkdir() + source = case / relative / "plugin.prx" + original = bytes(module_with_symbol(True)) + source.write_bytes(original) + result, output = convert(case, windows, ["--module-dir", name]) + assert result.returncode == 0, (name, result.stdout, result.stderr) + artifact = case / "app0" / relative / "plugin.prx.guest.prx" + assert list(case.rglob("*.guest.prx")) == [artifact] + assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + assert source.read_bytes() == original + if windows: + assert "__aps5_guest_initialize" in pe_export_names(artifact.read_bytes()), name + else: + assert not needed_libraries(output.read_bytes()), name + if windows and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + + case = work / f"{windows}-rooted" + (case / "Media" / "Plugins").mkdir(parents=True) + (case / "..plugins").mkdir() + external = work / f"{windows}-external" + external.mkdir() + original = bytes(module_with_symbol(True)) + for directory in (external, case / "..plugins"): + (directory / "plugin.prx").write_bytes(original) + rejected = [str(external), "../" + external.name, "Media/../../" + external.name] + if os.name == "nt": + rejected.extend((str(external)[len(external.drive):], case.drive + "..plugins", case.drive)) + for name in rejected: + result, output = convert(case, windows, ["--module-dir", name], cwd=case) + assert result.returncode == 2 and "inside the guest module parent directory" in result.stderr, (name, result.stderr) + assert not output.exists() and not list(case.rglob("*.guest.prx")) + assert (external / "plugin.prx").read_bytes() == original print("Guest module directory option tests passed") From 42ac947f9048bdf772472423f1608cda3b2f3e28 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 17:14:27 +0000 Subject: [PATCH 188/277] fix(agc): wake queue zero when workers become idle --- .../libSceAgcDriver/Execution/include/Driver/Driver.hpp | 1 + .../Execution/src/Driver/Queues/Submission.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index cc387b5e7..b5659313b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -343,6 +343,7 @@ private: std::uint64_t queue0Executing = 0; std::atomic orderHolders{0}; std::atomic runningWorkers{0}; + std::atomic idleEpoch{0}; std::atomic queue0Awaited{0}; std::atomic flipsAhead{0}; std::atomic flipHolders{0}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index a4fbb6e4a..a0db6f839 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -332,8 +332,12 @@ bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const { } void Driver::noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked) { - if (blocked) runningWorkers.fetch_sub(1, std::memory_order_acq_rel); - else runningWorkers.fetch_add(1, std::memory_order_acq_rel); + if (blocked) { + if (runningWorkers.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } else runningWorkers.fetch_add(1, std::memory_order_acq_rel); if (queue == 0) queue0Awaited.store(blocked ? awaited : 0, std::memory_order_release); if (queue == 0 && !blocked) queue0AwaitsTitle.store(false, std::memory_order_release); if (blocked && orderHolders.load(std::memory_order_acquire) != 0) { From 8a0395381ec0a9e031a550ab4e8cfb9cb50f5ced Mon Sep 17 00:00:00 2001 From: krishnaraya Date: Sun, 11 Oct 2026 13:16:11 -0400 Subject: [PATCH 189/277] fix(shader): accept DPP8 on v_pk_fmac_f16 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/execution/Dpp8PkFmac.cpp | 113 ++++++++++++++++++ .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 1 - 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp new file mode 100644 index 000000000..af31f0874 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp @@ -0,0 +1,113 @@ +#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 MaxThreads = 64; +constexpr std::uint32_t Stride = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::array Code(std::uint32_t selectors) { + const auto dpp8 = [&](std::uint32_t source) { return (selectors << 8u) | source; }; + return { + 0x34080084u, + 0xe03c1000u, 0x80000104u, + 0xbf8c3f70u, + 0x7e140301u, + 0x781406e9u, dpp8(2u), + 0x7e1a02e9u, dpp8(2u), + 0x7e160301u, + 0x7816070du, + 0x7e180301u, + 0x78180702u, + 0xe07c1000u, 0x80010a04u, + 0xbf810000u, + }; +} + +std::uint32_t Selectors(const std::array& lanes) { + std::uint32_t value = 0; + for (std::uint32_t lane = 0; lane < 8u; ++lane) value |= lanes[lane] << (lane * 3u); + return value; +} + +std::uint16_t Half(std::uint32_t seed) { + const auto sign = (seed >> 15u) & 1u; + const auto exponent = 10u + seed % 10u; + const auto mantissa = (seed >> 4u) & 0x3ffu; + return static_cast((sign << 15u) | (exponent << 10u) | mantissa); +} + +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t wave, const std::array& lanes, const std::string& what) { + for (std::uint32_t lane = 0; lane < MaxThreads; ++lane) { + for (std::uint32_t word = 0; word < 3u; ++word) { + const auto seed = lane * 2654435761u + word * 40503u + 12345u; + Input[lane * Stride + word] = static_cast(Half(seed)) | (static_cast(Half(seed * 7u + 3u)) << 16u); + } + } + Output.fill(0xdeadbeefu); + const auto code = Code(Selectors(lanes)); + std::vector userData; + for (const auto& descriptor : {BufferDescriptor(Input.data(), sizeof(Input)), BufferDescriptor(Output.data(), sizeof(Output))}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); + } + const std::span program(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(program)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{wave, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), program, 0, {}}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(wave), + {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(); + bool permuted = false; + for (std::uint32_t lane = 0; lane < wave; ++lane) { + const auto fused = Output[lane * Stride]; + const auto separate = Output[lane * Stride + 1u]; + const auto plain = Output[lane * Stride + 2u]; + Require(fused == separate, what + " (wave" + std::to_string(wave) + "): lane " + std::to_string(lane) + " v_pk_fmac_f16 dpp8 gave " + std::to_string(fused) + + ", v_mov_b32 dpp8 then v_pk_fmac_f16 gave " + std::to_string(separate)); + permuted = permuted || fused != plain; + } + Require(permuted, what + " (wave" + std::to_string(wave) + "): the dpp8 result equals the unpermuted one in every lane"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t wave : {32u, 64u}) { + Run(*device, wave, {7, 6, 5, 4, 3, 2, 1, 0}, "reversed groups of eight"); + Run(*device, wave, {1, 0, 3, 2, 5, 4, 7, 6}, "swapped neighbours"); + } + std::puts("dpp8 v_pk_fmac_f16 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 0e6078d53..7f5ab67f2 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -723,7 +723,6 @@ bool hasDppForm(RdnaOpcode opcode) { bool acceptsDpp8(RdnaOpcode opcode) { switch (opcode) { - case RdnaOpcode::VPkFmacF16: case RdnaOpcode::VMovrelsB32: case RdnaOpcode::VMovrelsdB32: case RdnaOpcode::VMovrelsd2B32: From f4424b5f1ec418cb029df76eaf7607a4e1025a37 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 10:49:33 +0200 Subject: [PATCH 190/277] feat(libSceAgc): implement the vieBRwlh1Lw interpolant mapping update Hollow Knight Silksong (PPSA12544) imports libSceAgc NID vieBRwlh1Lw (#699). Its name is unknown: it is not in aerolib, and the sceAgcUpdateInterpolantMapping label other projects give it hashes to SbuY2jN+axQ. OpenAGC (src/game_compat.c at 3c7e807, sceAgcUnknownVieBRwlh1Lw) reads it from FW 11.60 as the dbOlWdppb4o transform with the same (regs, gs, ps) arguments, without the identity tail: it writes one register per pixel shader input, none for a pixel shader without inputs, and rejects a null pixel shader. It now shares the dbOlWdppb4o path and throws for the rejected arguments. For is_f16 1 and 3 inputs whose default_value and default_value_hi differ, OpenAGC takes DEFAULT_VAL from default_value_hi, while KytyPS5, sharpemu and dbOlWdppb4o here take it from default_value; these inputs throw. On every other input the dbOlWdppb4o values equal OpenAGC's. agc_interpolant_mapping runs the dbOlWdppb4o vectors and OpenAGC's enhanced-mapping vectors through it. Co-Authored-By: Claude Fable 5.1 --- .../Shader/src/InterpolantMapping.cpp | 36 +++++++--- core/libs/prx/libSceAgc/Unimplemented.cpp | 6 -- .../libSceAgc/tests/InterpolantMapping.cpp | 71 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 3 +- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f4..608c859ad 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,10 +53,21 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) { +std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t f16 = (psWord >> 20u) & 0x3u; + if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { + throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); + } + return CreateInterpolantValueSplitF16(psWord, gsSemantic); +} + +int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { if (regs == nullptr) { throw std::runtime_error(std::string(fn) + ": regs is null"); } + if (ps == nullptr && !fillIdentityTail) { + throw std::runtime_error(std::string(fn) + ": ps is null"); + } if (ps != nullptr && ps->num_input_semantics > 32) { throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } @@ -66,13 +77,14 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* auto* output = regs; regs = values.data(); if (ps == nullptr || ps->num_input_semantics == 0) { - FillIdentityInterpolants(regs, 0); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0; + if (fillIdentityTail) FillIdentityInterpolants(regs, 0); if (ps != nullptr) { - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); } transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic)); } - FillIdentityInterpolants(regs, ps->num_input_semantics); - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics; + if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -111,12 +124,17 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* extern "C" { int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); +} + +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 860242c9d..0b85ef7ff 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -90,10 +90,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 1ea1d6ca2..b176809a4 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -13,12 +13,14 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { using Registers = std::array; const Shader* mappedPixel = nullptr; Registers preparedMapping{}; +std::size_t preparedCount = 0; unsigned mappings = 0; unsigned links = 0; @@ -65,6 +67,11 @@ void testIdentity() { regs = filled(); check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); + regs = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(preparedCount == 0u, "update without inputs prepared registers"); + const auto untouched = filled(); + check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); } void testMapping() { @@ -99,6 +106,15 @@ void testMapping() { check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed"); checkIdentity(split, static_cast(inputs.size()), "unused interpolants are not the identity"); + auto update = filled(); + const auto untouched = filled(); + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(preparedCount == inputs.size(), "update prepared registers past its inputs"); + check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); + check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + auto regular = filled(); check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed"); check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers"); @@ -106,6 +122,51 @@ void testMapping() { checkIdentity(regular, static_cast(inputs.size()), "unused interpolants are not the identity"); } +void testUpdateMapping() { + std::array inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)}; + inputs[0].is_custom = 1; + inputs[0].default_value = 2; + inputs[1].default_value = 1; + inputs[1].default_value_hi = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_custom = 1; + inputs[3].default_value = 3; + inputs[3].default_value_hi = 3; + std::array outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + + auto update = filled(); + const auto untouched = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(update[0].value == 0x00000625u, "custom interpolant update changed"); + check(update[1].value == 0x01380107u, "low f16 half update changed"); + check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); + check(update[3].value == 0x03680709u, "full f16 interpolant update changed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed"); + } + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + + auto create = filled(); + check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + for (const std::uint32_t index : {1u, 3u}) { + inputs[index].default_value = 0; + update = filled(); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); + inputs[index].default_value = inputs[index].default_value_hi; + } +} + void testRejections() { std::array inputs{}; Shader gs{}; @@ -117,18 +178,23 @@ void testRejections() { const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); } } extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { - check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); + check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); mappedPixel = shader; std::copy(context.begin(), context.end(), preparedMapping.begin()); + preparedCount = context.size(); ++mappings; } @@ -141,8 +207,9 @@ int main() { try { testIdentity(); testMapping(); + testUpdateMapping(); testRejections(); - check(mappings == 3u && links == 2u, "mapping preparation call count changed"); + check(mappings == 7u && links == 5u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..3827dbf51 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -221,7 +221,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [sceNpSessionSignalingGetConnectionFromPeerAddress](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [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 - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw @@ -597,3 +597,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` From 58669dc134210166b22d240d52762399e0924c87 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:22:44 -0400 Subject: [PATCH 191/277] fix(libc): write each log line in one call mingw's fprintf writes an unbuffered stream a character at a time and holds the stream's lock for the whole line. The APS5_LOG_* lines that carry a RecompileRequest ("Shader ... could not be prepared after registration", 15-20 KB each) took one WriteFile per character, and every other thread writing to stderr waited behind them, guest calls that log included. Demon's Souls (PPSA01342 v1.005, Windows) stops on a black screen after two frames on main: a BPE job worker waits for the stderr lock in sceSaveDataDirNameSearch's trace while the shader preparation pool prints its dumps, so GameCoreLoop never finishes a frame. The log macros now format the line into memory and write it with one fwrite, as Driver::reportSkip does since 8cf2f6580. The UCRT writes an unbuffered stream's fwrite as one even-length block and the last byte, so a line takes at most two writes on Windows and one on Linux. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 9 ++ .../prx/libc/include/general/LogMacros.hpp | 33 ++++++- core/libs/tests/LogMacros.cpp | 89 +++++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/LogMacros.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ddfe85664..5fd20977e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -491,6 +491,15 @@ if(BUILD_TESTING) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) endif() + add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) + target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(log_macros_tests PRIVATE libc) + configure_windows_unwind(log_macros_tests) + add_test(NAME log_macros COMMAND log_macros_tests) + if(WIN32) + set_tests_properties(log_macros PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) diff --git a/core/libs/prx/libc/include/general/LogMacros.hpp b/core/libs/prx/libc/include/general/LogMacros.hpp index 9142a9c73..c10306ecc 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,7 +1,34 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP +#include +#include #include +#include + +#ifdef __MINGW_PRINTF_FORMAT +[[gnu::format(__MINGW_PRINTF_FORMAT, 2, 3)]] +#else +[[gnu::format(printf, 2, 3)]] +#endif +inline int WriteLogLine(std::FILE* stream, const char* format, ...) { + char small[1024]; + std::va_list arguments; + va_start(arguments, format); + std::va_list again; + va_copy(again, arguments); + const int length = std::vsnprintf(small, sizeof(small), format, arguments); + va_end(arguments); + std::string large; + if (length >= static_cast(sizeof(small))) { + large.resize(static_cast(length)); + std::vsnprintf(large.data(), large.size() + 1, format, again); + } + va_end(again); + if (length < 0) return length; + std::fwrite(large.empty() ? small : large.data(), 1, static_cast(length), stream); + return std::fflush(stream); +} inline const char* TrimNidPostfix(const char* func) { return func; @@ -30,12 +57,10 @@ constexpr const char* BaseName(const char* path) { #define _APS5_FILE_ BaseName(__FILE__) #define _APS5_LOG_IMPL(stream, fmt, ...) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__) #define _APS5_LOG_IMPL_NF(stream, fmt) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)) #define APS5_LOG_OUT(fmt, ...) _APS5_LOG_IMPL(stdout, fmt, __VA_ARGS__) #define APS5_LOG_ERR(fmt, ...) _APS5_LOG_IMPL(stderr, fmt, __VA_ARGS__) diff --git a/core/libs/tests/LogMacros.cpp b/core/libs/tests/LogMacros.cpp new file mode 100644 index 000000000..7106aa212 --- /dev/null +++ b/core/libs/tests/LogMacros.cpp @@ -0,0 +1,89 @@ +#include "prx/libc/include/general/LogMacros.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#endif + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "log macros test failed: %s\n", what); + std::exit(1); +} + +template +std::vector CaptureWrites(TAction action) { + std::vector messages; +#ifdef _WIN32 + const std::string name = R"(\\.\pipe\anyps5-log-macros-)" + std::to_string(GetCurrentProcessId()); + HANDLE server = CreateNamedPipeA(name.c_str(), PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1, 0, 1 << 20, 0, nullptr); + Require(server != INVALID_HANDLE_VALUE, "message pipe"); + HANDLE client = CreateFileA(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + Require(client != INVALID_HANDLE_VALUE, "message pipe client"); + std::FILE* stream = _fdopen(_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY), "wb"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + DWORD read = 0; + while (ReadFile(server, buffer.data(), static_cast(buffer.size()), &read, nullptr)) messages.emplace_back(buffer.data(), read); + }); +#else + int sockets[2]; + Require(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) == 0, "message socket pair"); + std::FILE* stream = fdopen(sockets[0], "w"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + ssize_t read = 0; + while ((read = recv(sockets[1], buffer.data(), buffer.size(), 0)) > 0) messages.emplace_back(buffer.data(), static_cast(read)); + }); +#endif + Require(stream != nullptr, "message stream"); + Require(std::setvbuf(stream, nullptr, _IONBF, 0) == 0, "unbuffered message stream"); + action(stream); + std::fclose(stream); + reader.join(); +#ifdef _WIN32 + CloseHandle(server); +#else + close(sockets[1]); +#endif + return messages; +} + +std::string Join(const std::vector& messages) { + std::string text; + for (const auto& message : messages) text += message; + return text; +} + +bool HasLine(const std::string& text, const std::string& tail) { + const std::string head = "[tests/LogMacros.cpp:"; + return text.size() > head.size() + tail.size() && text.compare(0, head.size(), head) == 0 && text.compare(text.size() - tail.size(), tail.size(), tail) == 0 && text.find('\n') == text.size() - 1; +} + +} + +int main() { + const auto shortLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "value %d of %s", 7, "seven") == 0, "a short line was not flushed"); }); + Require(HasLine(Join(shortLine), "] value 7 of seven\n"), "a short line differs"); + Require(shortLine.size() <= 2, "a short line took more than two writes"); + + const std::string text(20000, 'r'); + const auto longLine = CaptureWrites([&](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "%s", text.c_str()) == 0, "a long line was not flushed"); }); + Require(HasLine(Join(longLine), "] " + text + "\n"), "a long line differs"); + Require(longLine.size() <= 2, "a 20000-character line took more than two writes"); + + const auto plainLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL_NF(stream, "plain") == 0, "a line without arguments was not flushed"); }); + Require(HasLine(Join(plainLine), "] plain\n"), "a line without arguments differs"); + Require(plainLine.size() <= 2, "a line without arguments took more than two writes"); + return 0; +} From f8ed012e3e30e7b331c410c11ec0b880b69fac0f Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 17:26:18 +0000 Subject: [PATCH 192/277] perf(recompiler): keep relatively addressed vector registers in a window array `v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32` and `v_swaprel_b32` lowered to a compare-and-select chain over every register from the base to the program's last allocated register, so a program addressing 154 registers 104 times turned 3176 ISA words into 649,515 SPIR-V words that Intel's compiler did not finish. The registers from the lowest relative base up now live in a function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`), so an indexed access is one load or store; plain accesses to those registers go through the same array, DPP-modified relative writes keep their `DppUpdateU32` merge, an index past the window reads 0 and drops its write, and programs without relative addressing are unchanged. The same program recompiles to 392,333 words with 4,992 selects instead of 32,383. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../libSceAgcDriver/tests/VectorWindow.cpp | 123 ++++++++++++++++++ .../IntermediateRepresentation/IrBuilder.hpp | 1 + .../IrMetadata/ProgramMetadata.hpp | 2 + .../IntermediateRepresentation/IrOpcode.hpp | 2 + .../src/IrBuilder/IrBuilderRegisters.cpp | 14 ++ .../src/IrOpcode.cpp | 3 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 2 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterRegisters.cpp | 18 +++ .../SpirvBackend/src/SpirvFlowEmitter.cpp | 2 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 10 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 5 + .../src/ControlFlowInstructions.cpp | 31 +++++ .../Translation/src/InstructionTranslator.cpp | 26 ++++ docs/dev/TechnicalDebt.md | 1 + 16 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..a17954a78 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -485,6 +485,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_masked_select_tests) +add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp) +target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vector_window_tests) + add_test_executable(agc_constant_bit_test_tests EXCLUDE_FROM_ALL tests/ConstantBitTest.cpp) target_link_libraries(agc_constant_bit_test_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_constant_bit_test_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1001,7 +1006,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_frontend_pair agc_shader_frontend_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_constant_bit_test_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 ${agcExecutionTests}) + 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_vector_window_tests agc_constant_bit_test_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 ${agcExecutionTests}) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp new file mode 100644 index 000000000..3e852184b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -0,0 +1,123 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +constexpr std::uint32_t MoveM0Two = 0xbefc0382u; +constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; +constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; +constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; +constexpr std::uint32_t MovreldV4V1 = 0x7e088501u; +constexpr std::uint32_t MoveV1V5 = 0x7e020305u; +constexpr std::uint32_t EndProgram = 0xbf810000u; + +struct Shape { + std::size_t functionArrays = 0; + std::uint32_t arrayLength = 0; + std::size_t accessChains = 0; + std::size_t selects = 0; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RecompileProgram(std::span code) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; + const std::array userData{}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + 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; + auto result = Recompile(request); + Require(!result.spirv.empty(), "the relative-addressing program did not recompile"); + return std::vector(result.spirv.Words().begin(), result.spirv.Words().end()); +} + +Shape Scan(std::span words) { + Shape shape; + std::vector> constants; + std::vector> arrays; + std::vector> functionPointers; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpConstant && count == 4) constants.emplace_back(words[cursor + 2], words[cursor + 3]); + if (op == spv::OpTypeArray) arrays.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction) { + for (const auto& [pointer, pointee] : functionPointers) { + if (pointer != words[cursor + 1]) continue; + for (const auto& [array, length] : arrays) { + if (array != pointee) continue; + ++shape.functionArrays; + for (const auto& [constant, value] : constants) { + if (constant == length) shape.arrayLength = value; + } + } + } + } + if (op == spv::OpAccessChain) ++shape.accessChains; + if (op == spv::OpSelect) ++shape.selects; + cursor += count; + } + return shape; +} + +bool run(const char* name, bool (*test)()) { + try { + if (test()) return true; + std::fprintf(stderr, "%s\n", name); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + } + return false; +} + +} + +int main() { + bool passed = true; + + passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] { + const std::array code{MoveM0Two, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, MovreldV4V1, MoveV1V5, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2 || shape.selects > 4) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); + return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2 && shape.selects <= 4; + }); + + passed &= run("a program without relative addressing keeps its registers in SSA values", [] { + const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + return shape.functionArrays == 0 && shape.accessChains == 0; + }); + + return passed ? 0 : 1; +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp index 99b5ef6fa..39c7c07c3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp @@ -88,6 +88,7 @@ public: } private: + IrValue* vectorWindowIndex(VectorReg reg); IrProgram& program; IrBlock* insertionPoint; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 9a6b86e8b..67eedc48c 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -26,6 +26,8 @@ struct IrProgramMetadata { IrBindingLayout bindings; bool bindingLayoutComplete = false; std::uint32_t tessellationLocalStride = 0; + std::uint32_t vectorWindowBase = 0; + std::uint32_t vectorWindowSize = 0; std::uint32_t tessellationControlStride = 0; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index e580b1118..6f0d51c54 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -93,6 +93,8 @@ enum class IrOpcode : std::uint16_t { SetScalarRegister, GetVectorRegister, SetVectorRegister, + LoadVectorWindow, + StoreVectorWindow, GetGotoVariable, SetGotoVariable, GetScc, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp index 2a81dd39b..3f45934d4 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp @@ -1,6 +1,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrBuilderInternal.hpp" #include +#include namespace ShaderRecompiler { @@ -106,12 +107,25 @@ void IrBuilder::SetScalarMaskTag(ScalarReg reg, IrValue& value) { (void)Emit(IrOpcode::SetScalarMaskTag, IrType::Void, {&operand, &value}); } +IrValue* IrBuilder::vectorWindowIndex(VectorReg reg) { + const auto& metadata = program.Metadata(); + const auto index = static_cast(RegIndex(reg)); + if (metadata.vectorWindowSize == 0u || index < metadata.vectorWindowBase) return nullptr; + if (index - metadata.vectorWindowBase >= metadata.vectorWindowSize) throw std::runtime_error("vector register " + std::to_string(index) + " lies past the relative-addressing window"); + return &Constant(index - metadata.vectorWindowBase); +} + IrValue& IrBuilder::GetVectorReg(VectorReg reg) { + if (IrValue* index = vectorWindowIndex(reg)) return Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); return Emit(IrOpcode::GetVectorRegister, IrType::U32, {&operand}); } void IrBuilder::SetVectorReg(VectorReg reg, IrValue& value) { + if (IrValue* index = vectorWindowIndex(reg)) { + (void)Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &value}); + return; + } IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); (void)Emit(IrOpcode::SetVectorRegister, IrType::Void, {&operand, &value}); } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 7abf9208a..67f8993f6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -134,6 +134,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("SetScalarRegister", Void, ScalarReg, U32), makeMeta("GetVectorRegister", U32, VectorReg), makeMeta("SetVectorRegister", Void, VectorReg, U32), + makeMeta("LoadVectorWindow", U32, U32), + makeMeta("StoreVectorWindow", Void, U32, U32), makeMeta("GetGotoVariable", U1, U32), makeMeta("SetGotoVariable", Void, U32, U1), makeMeta("GetScc", U1), @@ -890,6 +892,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::SetScalarMaskTag: case IrOpcode::SetScalarRegister: case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: case IrOpcode::SetGotoVariable: case IrOpcode::SetScc: case IrOpcode::SetExec: diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 97071e7ca..7cdaf8d9c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -388,6 +388,8 @@ void EmitGetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetScc(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 23288d4c7..a876a4062 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -147,6 +147,7 @@ struct SpirvEmitterState { std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; + std::array vectorWindowVariable {}; std::array imageVariables {}; std::uint32_t samplerVariable = 0; std::uint32_t mainFunc = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index d46db4461..f24270d40 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include @@ -44,6 +45,23 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } +std::uint32_t VectorWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), ctx.Arg(inst, 0u)); + return pointer; +} + +void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { + const auto value = ctx.state.module.AllocateId(); + ctx.state.module.AddFunction(spv::OpLoad, TypeU32(ctx.state), value, VectorWindowElement(ctx, inst)); + ctx.Define(inst, value); +} + +void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { + ctx.state.module.AddFunction(spv::OpStore, VectorWindowElement(ctx, inst), ctx.Arg(inst, 1u)); +} + void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 59a389eb2..7471dfbfb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -308,6 +308,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::SetScalarRegister: return Invoke(EmitSetScalarRegister, ctx, inst); case IrOpcode::GetVectorRegister: return Invoke(EmitGetVectorRegister, ctx, inst); case IrOpcode::SetVectorRegister: return Invoke(EmitSetVectorRegister, ctx, inst); + case IrOpcode::LoadVectorWindow: return Invoke(EmitLoadVectorWindow, ctx, inst); + case IrOpcode::StoreVectorWindow: return Invoke(EmitStoreVectorWindow, ctx, inst); case IrOpcode::GetGotoVariable: return Invoke(EmitGetGotoVariable, ctx, inst); case IrOpcode::SetGotoVariable: return Invoke(EmitSetGotoVariable, ctx, inst); case IrOpcode::GetScc: return Invoke(EmitGetScc, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index db279fdf5..699290232 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -55,6 +55,11 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } + if (state.program.Metadata().vectorWindowSize != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.vectorWindowVariable.at(half) = state.module.AllocateId(); + } + } state.mainFunc = state.module.AllocateId(); if (StageOf(state) == IrShaderStage::Mesh) { const auto& mesh = VertexInfo(state).mesh; @@ -204,6 +209,11 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddName(state.scratchVariable.at(half), "scratch_dwords"); } } + if (state.program.Metadata().vectorWindowSize != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddName(state.vectorWindowVariable.at(half), "vector_window"); + } + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 5884af305..f2bd0fde9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -999,6 +999,11 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, state.program.Info().scratchDwords), state.scratchVariable.at(half), spv::StorageClassFunction); } } + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, window), state.vectorWindowVariable.at(half), spv::StorageClassFunction); + } + } if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 838346dc4..8bf064fb1 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -295,10 +295,28 @@ void TranslationContext::sWqm(const RdnaInstruction& inst, bool wide) { // M0 holds a uniform register offset. Vector registers are SSA values with fixed indices, so an indexed // access becomes a select over every register the program references at or past the base. +struct RelativeWindowAccess { + IrValue* index = nullptr; + IrValue* inRange = nullptr; +}; + +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset) { + const auto& metadata = program.Metadata(); + if (metadata.vectorWindowSize == 0u) return {}; + if (reg < metadata.vectorWindowBase) throw std::runtime_error("relative vector access below the relative-addressing window"); + IrValue& raw = ir.IAdd(ir.Constant(reg - metadata.vectorWindowBase), offset); + IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&raw, &ir.Constant(metadata.vectorWindowSize - 1u)}); + return {&index, &ir.ULessThan(raw, ir.Constant(metadata.vectorWindowSize))}; +} + IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name) { if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } + if (const auto access = relativeWindowAccess(ir, program, base.reg, offset); access.index != nullptr) { + IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.Constant(0u)))); + } IrU32 result(ir.GetVectorReg(static_cast(base.reg))); for (std::uint32_t reg = base.reg + 1u; reg < currentVectorLimit; ++reg) { IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); @@ -312,6 +330,19 @@ void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& o if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } + if (const auto access = relativeWindowAccess(ir, program, destination.reg, offset); access.index != nullptr) { + IrValue* index = access.index; + IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else { + merged = &ir.Select(ir.GetExec(), value.Value(), previous); + } + (void)ir.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &ir.Select(*access.inRange, *merged, previous)}); + return; + } for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { RdnaOperand target = destination; target.reg = reg; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 421ea460d..9df2181a8 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -396,6 +396,32 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con IrProgram program; program.SetWaveSize(options.waveSize); + std::uint32_t windowBase = NumVectorRegs; + for (const auto& cfgBlock : cfg.blocks) { + for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { + const auto& instruction = decoded.instructions[index]; + if (isCodeTableLoad(cfg, instruction.programCounter)) continue; + switch (instruction.op) { + case RdnaOpcode::VMovrelsB32: + windowBase = std::min(windowBase, instruction.source0.reg); + break; + case RdnaOpcode::VMovreldB32: + windowBase = std::min(windowBase, instruction.destination.reg); + break; + case RdnaOpcode::VMovrelsdB32: + case RdnaOpcode::VMovrelsd2B32: + case RdnaOpcode::VSwaprelB32: + windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); + break; + default: + break; + } + } + } + if (windowBase < vectorLimit) { + program.Metadata().vectorWindowBase = windowBase; + program.Metadata().vectorWindowSize = vectorLimit - windowBase; + } program.Resources().stage = toIrShaderStage(options.stage); program.Resources().shaderHash = options.shaderHash; program.Resources().userDataBase = options.userDataBaseRegister; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..719999491 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -577,6 +577,7 @@ Throughout the project, every function at every stage either **does exactly what - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to its last allocated register in a 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. An index past the window reads 0 and its write is dropped, as the RDNA ISA describes for out-of-range VGPR indexing, where the earlier select chain read the base register and dropped the write; neither is measured on the console. Killzone: Liberation's PSP vertex program (PPSA11142) went from 649,515 to the recompiled size noted in its pull request with this. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws - [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. From d40189d707d3b283c66a1f749f4b3ef8aa910d1a Mon Sep 17 00:00:00 2001 From: omurc4 Date: Sun, 11 Oct 2026 13:08:06 -0400 Subject: [PATCH 193/277] feat(libSceVideoOut): export sceVideoOutAddBufferAttributeOption Elden Ring Nightreign (PPSA25380) imports the function, which no library exported, so the title failed to load. The option is ORed into the buffer attribute and a null attribute throws. Options other than 0 and 8 still throw when the buffers are registered. --- core/libs/prx/libSceVideoOut/include/Buffer.hpp | 1 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 7 +++++++ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 10 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 19 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 903e4029c..41a36673a 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -10,6 +10,7 @@ extern "C" { int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_index_start, const VideoOutBuffers* buffers, int buffer_num, const VideoOutBufferAttribute2* attribute, int category, void* option); void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color); +void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index, const VideoOutBufferAttribute2* attribute, void* option); int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_t flip_arg); int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int set_index); diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 481c92c2f..965e37b3d 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -47,6 +47,13 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut attribute->dcc_control = dccControl; } +void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutAddBufferAttributeOption: null attribute"); + } + attribute->option |= option; +} + int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 607870626..85d87719a 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -233,6 +233,16 @@ void testDecode() { group.attribute.option = option; check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted"); } + sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0, 0); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY && group.attribute.width == 259 && group.attribute.height == 137, "an added buffer option was not stored"); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY, "adding a buffer option twice changed it"); + check(DescribeVideoOutBuffer(registered, group).address == plain.address, "a buffer with an added supported option was rejected"); + sceVideoOutAddBufferAttributeOption(&group.attribute, 0x20); + check(group.attribute.option == 0x28, "an added buffer option did not keep the existing bits"); + check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option 40") != std::string::npos, "an unsupported added buffer option was accepted"); + check(expectFailure([&] { sceVideoOutAddBufferAttributeOption(nullptr, 0x20); }).find("null attribute") != std::string::npos, "a null attribute was accepted"); VideoOutBuffer dccBuffer{0, reinterpret_cast(tiled.data()), 0x7f0000}; sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344); group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..848ebfd7a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -95,6 +95,7 @@ Throughout the project, every function at every stage either **does exactly what - [Command buffer cursors](../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [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 - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it +- [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 - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins From 73866db75584a86eb072bc0e816c274c9a4077cf Mon Sep 17 00:00:00 2001 From: gookie <36005381+gookie-dev@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:26:30 +0200 Subject: [PATCH 194/277] fix(pad): report special ports as not connected in scePadOpen scePadOpen returned the standard pad's handle for a special port (type 2) as well, and scePadGetHandle found it there. Both ports then read the one input queue, so a press queued between two frames could be drained by the special port's read and never reach the standard port. PPSA16827 (Harmony's Odyssey) opens both ports for the initial user and reads both every frame, also polling controller information and device class data for the special one. Special ports now return DEVICE_NOT_CONNECTED (0x80920007) from scePadOpen, as scePadOpenExt already does because the host has no special controller, and scePadGetHandle has no handle for them. TechnicalDebt's scePadOpenExt entry now covers both calls. guest_pad opens a special port before and after the standard one and expects DEVICE_NOT_CONNECTED, and DEVICE_NO_HANDLE from scePadGetHandle for the special port while the standard handle stays valid. It fails on main. PPSA16827 over 130 s of its title screen: 6812 scePadRead, 3408 scePadGetControllerInformation and 3406 scePadDeviceClassParseData calls before, two reads of the same queue per frame; 2702 reads, one information call and no parse calls after. --- core/libs/prx/libScePad/Export.cpp | 3 ++- core/libs/tests/GuestPad.cpp | 6 +++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 38bb59ca5..4338e1288 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -73,7 +73,7 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } int APS5_VABI scePadGetHandle(int user_id, int type, int index) { - if (!g_opened || !ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; + if (!g_opened || !ValidPort(user_id, type, index) || type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NO_HANDLE; return PAD_HANDLE; } @@ -136,6 +136,7 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void if (!ValidPort(userId, type, index)) { return PAD_ERROR_INVALID_ARG; } + if (type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NOT_CONNECTED; g_opened = true; return PAD_HANDLE; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 992f0f7b9..454d8de60 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -183,10 +183,14 @@ int main() { Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 1, wheel) == PAD_ERROR_INVALID_ARG); Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); + Require(scePadGetHandle(user, 0, 0) == noHandle); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); Require(scePadGetHandle(user, 0, 0) == handle); - Require(scePadGetHandle(user, 2, 0) == handle); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadGetHandle(user, 0, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); CheckReadQueue(handle); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f2bfa00ca 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -83,7 +83,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcWaitRegMemPatchMask](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws -- [scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - only special ports (type 2) are handled, and they return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller; whether the console opens a handle and reports the device as disconnected later is unknown. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list From 0418aaaad287980c56407eda9c3cbd747c688815 Mon Sep 17 00:00:00 2001 From: Daan Dehing Date: Sun, 11 Oct 2026 19:28:27 +0200 Subject: [PATCH 195/277] fix(agc): draw the complete rectangles of a rect-list draw with a partial last one A rect-list draw whose count is not a multiple of 3 threw "incomplete rect-list primitive". Rect lists are drawn as patch lists of 3 control points, and Vulkan generates floor(vertexCount/3) patches, so the draw now records as is and the trailing 1 or 2 vertices are left out. Palworld (PPSA23126) draws 4 vertices x 9 instances as a rect list when character creation opens (#3832). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 - core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++++++++-- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adb76dd93..3903f4050 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1025,7 +1025,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); - if (state.rectList) Require(draw.indexCount % 3 == 0, "incomplete rect-list primitive"); APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..2d73e4a33 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3025,8 +3025,17 @@ void rectListTests() { queue.userConfig[0x242] = 17; const auto state = DecodeState(queue); const Context context{}; - const AgcDriver::Pm4::DrawParameters draw{0, 4, 0, 1, 0, false}; - expectFailure([&] { Draw(context, state, draw, {}); }, "incomplete rect-list"); + const auto drawFailure = [&](std::uint32_t count) { + const AgcDriver::Pm4::DrawParameters draw{0, count, 0, 1, 0, false}; + try { + Draw(context, state, draw, {}); + } catch (const std::exception& error) { + return std::string(error.what()); + } + return std::string{}; + }; + const auto complete = drawFailure(3); + for (const auto count : {4u, 5u}) Require(drawFailure(count) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f391ebeff 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -357,6 +357,7 @@ Throughout the project, every function at every stage either **does exactly what - [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [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. - [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. - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline From 42062da564c2b29eaa91ef817e96c6f838a07314 Mon Sep 17 00:00:00 2001 From: nyavana Date: Sun, 11 Oct 2026 10:28:32 -0700 Subject: [PATCH 196/277] docs(build): clarify when explicit libs target is required --- docs/dev/BUILD.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 50e7023fe..cbcdd9d97 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -42,9 +42,11 @@ cmake --build build --target libs --parallel ``` `libs` is a custom target: every library under [core/libs/prx](../../core/libs/prx) is built with -`EXCLUDE_FROM_ALL`, so the first command alone does not produce them. Titles load the patched `.prx` -files from `build/core/libs/libs`, which only that second step refreshes. Running a title after a -library change without it therefore tests the previous binaries and can show no effect at all. +`EXCLUDE_FROM_ALL`. With the default `BUILD_TESTING=OFF`, the default build does not produce them, +so the explicit `--target libs` step is required. With `BUILD_TESTING=ON`, the default full build +also builds `libs` through the `patched_libraries_tests` dependency. Titles load the patched `.prx` +files from `build/core/libs/libs`; running a title after a library change without rebuilding `libs` +therefore tests the previous binaries and can show no effect at all. [Relinker usage and runtime layout](../user/USAGE.md). From c27e67ceb591ab1d7e07d6b6e4c47ebf7ebd88de Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 11 Oct 2026 19:31:41 +0200 Subject: [PATCH 197/277] test(libSceLibcInternal): run guest_force_tls_destructor on Windows sceKernelGetModuleInfoFromAddr has a Windows path since 77ef75c1d, so _sceLibcInternalForceTlsDestructor finds each dso's module there too. The test takes the other module's address with GetProcAddress where Linux uses dlsym(RTLD_NEXT), and the TechnicalDebt entry no longer says the call throws on Windows. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibcInternal/CMakeLists.txt | 15 +++++++++------ core/libs/tests/GuestForceTlsDestructor.cpp | 11 ++++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 40dbd1e76..3ad7a6878 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -39,11 +39,14 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) - target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) - add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) - set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) + target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_force_tls_destructor_tests) + add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) + set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_force_tls_destructor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/tests/GuestForceTlsDestructor.cpp b/core/libs/tests/GuestForceTlsDestructor.cpp index 49264c929..6f900b939 100644 --- a/core/libs/tests/GuestForceTlsDestructor.cpp +++ b/core/libs/tests/GuestForceTlsDestructor.cpp @@ -1,10 +1,15 @@ #include "SceTypes.hpp" #include #include -#include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); @@ -69,7 +74,11 @@ bool ForceThrows(KernelModule handle) { int main() { self = ModuleOf(&dsoHandle); +#ifdef _WIN32 + void* otherDso = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceLibcInternal.prx"), "_sceLibcInternalThreadDtors_nid_postfix")); +#else void* otherDso = dlsym(RTLD_NEXT, "_sceLibcInternalThreadDtors_nid_postfix"); +#endif Require(otherDso != nullptr); const KernelModule other = ModuleOf(otherDso); Require(other != self && other != 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..f804745e8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -188,7 +188,7 @@ Throughout the project, every function at every stage either **does exactly what - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_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 - [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 -- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors +- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does - [acosl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [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 - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. From fbb4d3771d2658690cf73a12529eceb49839df87 Mon Sep 17 00:00:00 2001 From: Lexroid Date: Sun, 11 Oct 2026 20:32:11 +0300 Subject: [PATCH 198/277] fix(libkernel): give AMM buffers their own queue One queue for APR and AMM buffers kept an AMM buffer behind an APR buffer waiting on an address that the AMM buffer writes, so neither ran. Marvel's Wolverine does this: an APR buffer waits on a counter that the WriteAddress commands of later AMM buffers advance, and its loading stopped. APR and AMM buffers now keep their order within their own queue. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 88 ++++++++++++------------- core/libs/tests/GuestAmpr.cpp | 23 +++++++ 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 1811d9b8a..b0e415ca6 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -676,70 +676,64 @@ struct AprQueue { struct Work { std::vector commands; Deferred resume; - std::uint32_t aprId = 0; - std::uint32_t ammId = 0; + std::uint32_t id = 0; }; std::deque pending; bool running = false; - std::uint32_t aprSubmitted = 0; - std::uint32_t aprCompleted = 0; - std::uint32_t ammSubmitted = 0; - std::uint32_t ammCompleted = 0; + std::uint32_t submitted = 0; + std::uint32_t completed = 0; }; -void _complete(AprQueue& queue, std::uint32_t aprId, std::uint32_t ammId) { - if (aprId != 0) queue.aprCompleted = std::max(queue.aprCompleted, aprId); - if (ammId != 0) queue.ammCompleted = std::max(queue.ammCompleted, ammId); +AprQueue* _startQueue() { + auto* created = new AprQueue(); + std::thread([created] { + for (;;) { + AprQueue::Work work; + { + std::unique_lock lock(created->mutex); + created->ready.wait(lock, [&] { return !created->pending.empty(); }); + work = std::move(created->pending.front()); + created->pending.pop_front(); + created->running = true; + } + try { + const auto bytes = static_cast(work.commands.size()); + _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, work.commands.data()}, work.resume.cursor, work.resume.read); + } catch (const std::exception& error) { + std::fprintf(stderr, "APR: %s\n", error.what()); + std::fflush(stderr); + std::abort(); + } + { + std::lock_guard lock(created->mutex); + created->running = false; + created->completed = std::max(created->completed, work.id); + } + created->done.notify_all(); + } + }).detach(); + return created; } -AprQueue& _queue() { - static AprQueue* queue = [] { - auto* created = new AprQueue(); - std::thread([created] { - for (;;) { - AprQueue::Work work; - { - std::unique_lock lock(created->mutex); - created->ready.wait(lock, [&] { return !created->pending.empty(); }); - work = std::move(created->pending.front()); - created->pending.pop_front(); - created->running = true; - } - try { - const auto bytes = static_cast(work.commands.size()); - _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, work.commands.data()}, work.resume.cursor, work.resume.read); - } catch (const std::exception& error) { - std::fprintf(stderr, "APR: %s\n", error.what()); - std::fflush(stderr); - std::abort(); - } - { - std::lock_guard lock(created->mutex); - created->running = false; - _complete(*created, work.aprId, work.ammId); - } - created->done.notify_all(); - } - }).detach(); - return created; - }(); - return *queue; +AprQueue& _queue(bool amm) { + static AprQueue* const queues[2] = {_startQueue(), _startQueue()}; + return *queues[amm ? 1 : 0]; } std::uint32_t _submit(const std::uint8_t* base, std::uint32_t bytes, bool amm) { - auto& queue = _queue(); + auto& queue = _queue(amm); std::lock_guard submitLock(queue.submit); std::uint32_t id; bool queued; { std::lock_guard lock(queue.mutex); - id = amm ? ++queue.ammSubmitted : ++queue.aprSubmitted; + id = ++queue.submitted; queued = queue.running || !queue.pending.empty(); } const auto enqueue = [&](Deferred resume) { { std::lock_guard lock(queue.mutex); - queue.pending.push_back({std::vector(base, base + bytes), resume, amm ? 0u : id, amm ? id : 0u}); + queue.pending.push_back({std::vector(base, base + bytes), resume, id}); } queue.ready.notify_one(); }; @@ -753,16 +747,16 @@ std::uint32_t _submit(const std::uint8_t* base, std::uint32_t bytes, bool amm) { } { std::lock_guard lock(queue.mutex); - _complete(queue, amm ? 0u : id, amm ? id : 0u); + queue.completed = std::max(queue.completed, id); } queue.done.notify_all(); return id; } void _waitSubmitted(std::uint32_t id, bool amm) { - auto& queue = _queue(); + auto& queue = _queue(amm); std::unique_lock lock(queue.mutex); - queue.done.wait(lock, [&] { return amm ? queue.ammCompleted >= id || id > queue.ammSubmitted : queue.aprCompleted >= id || id > queue.aprSubmitted; }); + queue.done.wait(lock, [&] { return queue.completed >= id || id > queue.submitted; }); } std::uint32_t AmmSubmit_nid_no_patch(void* base, std::uint32_t bytes) { diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index c4820323e..ff315c15a 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -329,6 +329,28 @@ void TestWaitSatisfiedAfterSubmission() { Require(sceKernelAprWaitCommandBuffer(firstId) == 0); } +void TestAmmNotBehindAprWait() { + Recorder reader; + Recorder maps; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + Require(sceAmprCommandBufferWaitOnAddress(&reader.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&reader.buffer, &done, 1) == 0); + Require(sceAmprAmmCommandBufferConstructor(&maps.buffer) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&maps.buffer, &flag, 1) == 0); + std::uint32_t readerId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&reader.buffer, 0, &readerId) == 0 && readerId != 0); + std::uint32_t mapsId = 0; + Require(sceAmprAmmSubmitCommandBuffer3(maps.memory.data(), maps.Offset(), 0, &mapsId) == 0 && mapsId != 0); + auto completed = std::async(std::launch::async, [&]() { return sceAmprAmmWaitCommandBufferCompletion(mapsId); }); + const bool returned = completed.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(completed.get() == 0); + Require(sceKernelAprWaitCommandBuffer(readerId) == 0); + Require(done == 1); +} + void TestCounters() { Recorder recorder; alignas(8) std::uint64_t single = 0; @@ -1074,6 +1096,7 @@ int main() { TestKernelEventQueue(); TestWaits(); TestWaitSatisfiedAfterSubmission(); + TestAmmNotBehindAprWait(); TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); From e2bfacefa32654ede5a6abfebf5850aa94860550 Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Sun, 11 Oct 2026 18:39:22 +0100 Subject: [PATCH 199/277] feat(libSceRudp): declare the calls PPSA06888 imports PPSA06888 stops on main at start-up because its bundled Media/Plugins/SceRudp.prx imports sceRudpEnableInternalIOThread2, sceRudpGetOption and sceRudpGetRemoteInfo, which no library exports. The signatures are unknown, so they are declared without parameters and throw. The title does not call them during start-up. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRudp/Export.cpp | 15 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 2 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 132ea291f..e402c18dd 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -101,6 +101,11 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } +int APS5_VABI sceRudpEnableInternalIOThread2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { std::lock_guard lk(g_mutex); g_handler = handler; @@ -150,6 +155,11 @@ int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32 return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; } +int APS5_VABI sceRudpGetOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpInitiate(int ctx_id, const void* to, uint32_t tolen, uint16_t vport) { (void)to; (void)tolen; @@ -178,6 +188,11 @@ int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { return 0; } +int APS5_VABI sceRudpGetRemoteInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpGetNumberOfPacketsToRead(int ctx_id) { std::lock_guard lk(g_mutex); if (!g_inited) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..b929e527a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -88,6 +88,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list - PPSA08427 imports declared without parameters, signatures unknown: [sceNpEntitlementAccessGetGameTrialsFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), imported by its bundled `pros.sdk.Prospero.prx` and not called before the main menu. Name from the aerolib NID list +- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too - [sceVoiceChatRequestCreateVoiceChatGroup, sceVoiceChatRequestJoinVoiceChatGroup, sceVoiceChatRequestLeaveVoiceChatGroup, sceVoiceChatRequestDeleteVoiceChatGroup](../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - assumed to take a request id and two pointers like the other `sceVoiceChatRequest*` calls; offline, every argument is ignored and `SIGNED_OUT` is returned, as for those - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceTextToSpeech2Initialize](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters From 780303049be5a52894f2cf1df476c2d641cd8eab Mon Sep 17 00:00:00 2001 From: Daan Dehing Date: Sun, 11 Oct 2026 19:39:47 +0200 Subject: [PATCH 200/277] test(agc): cover indexed rect-list draws with a partial last rectangle Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2d73e4a33..e03e6e2e6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3025,8 +3025,9 @@ void rectListTests() { queue.userConfig[0x242] = 17; const auto state = DecodeState(queue); const Context context{}; - const auto drawFailure = [&](std::uint32_t count) { - const AgcDriver::Pm4::DrawParameters draw{0, count, 0, 1, 0, false}; + std::array indices{}; + const auto drawFailure = [&](std::uint32_t count, bool indexed) { + const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast(indices.data()), count, 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, count, 0, 1, 0, false}; try { Draw(context, state, draw, {}); } catch (const std::exception& error) { @@ -3034,8 +3035,10 @@ void rectListTests() { } return std::string{}; }; - const auto complete = drawFailure(3); - for (const auto count : {4u, 5u}) Require(drawFailure(count) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); + for (const auto indexed : {false, true}) { + 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"); + } } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { From 9b1e1ecccd90d32e22710427d7bf7625501e6026 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+yunusshaikh7@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:12:19 -0700 Subject: [PATCH 201/277] fix(savedata): sort sceSaveDataDirNameSearch results by the requested key and order The search returned saves in host directory order and ignored the condition's key and order. A title that asks for the newest save first (modification time, descending) got whichever directory the host listed first: the alphabetically first on NTFS, an arbitrary one on Linux. Sort by directory name, user parameter or modification time, reverse for a descending order, and reject unknown keys and orders with the parameter error, as shadPS4 does. --- .../libs/prx/libSceSaveData.native/Export.cpp | 42 ++++++++++--- .../prx/libSceSaveData.native/SaveData.hpp | 8 +++ core/libs/tests/GuestSaveDataSearch.cpp | 63 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..a4475f9f4 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -270,13 +270,22 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS } result->hit_num = 0; result->set_num = 0; + const std::uint32_t key = cond->key; + 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; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } - std::uint32_t hit = 0; - std::uint32_t set = 0; + struct Match { + std::string name; + std::string path; + SaveDataParam param; + }; + const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; + std::vector matches; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; @@ -285,16 +294,29 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - hit++; - if (result->dir_names != nullptr && set < result->dir_names_num) { - std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); - if (result->params != nullptr) { - result->params[set] = load_param(entry.path().string()); - } - set++; + Match match{name, entry.path().string(), {}}; + if (sortByParam) { + match.param = load_param(match.path); + } + matches.push_back(std::move(match)); + } + std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; }); + if (key == SAVE_DATA_SORT_KEY_USER_PARAM) { + 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; }); + } + if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { + std::reverse(matches.begin(), matches.end()); + } + const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast(std::min(result->dir_names_num, matches.size())); + for (std::uint32_t i = 0; i < set; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str()); + if (result->params != nullptr) { + result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } } - result->hit_num = hit; + result->hit_num = static_cast(matches.size()); result->set_num = set; return SAVE_DATA_OK; } diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 20d02ad5a..ceffb09c0 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1; + constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index cc24049af..0be7b2d33 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -8,17 +8,79 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); 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); } +static constexpr int SaveDataErrorParameter = -2137063424; + static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + cond.order = order; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = capacity; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); + *hits = result.hit_num; + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void SortByKey() { + struct Save { + const char* name; + std::uint32_t userParam; + std::int64_t mtime; + }; + const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; + for (const auto& save : saves) { + std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 2; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); + Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); + Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + } + using Names = std::vector; + std::uint32_t hits = 0; + Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); + Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); + Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); + Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); + Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); + Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.key = 6; + SaveDataDirNameSearchResult result{}; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); + cond.key = 0; + cond.order = 2; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); +} + 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); @@ -64,6 +126,7 @@ int main() { int status = 0; try { SearchAndMount(); + SortByKey(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..d2bd4dca5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -283,6 +283,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 +- [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 - [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 - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature From e92547a7a1af60d3f7734a9e53217b6183a6bfeb Mon Sep 17 00:00:00 2001 From: Semir Kabir <66262518+semirkabir@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:48:06 +0000 Subject: [PATCH 202/277] feat(libkernel): wake each listed address in _umtx_op UMTX_OP_NWAKE_PRIVATE --- .../SyncOnAddress/src/SyncOnAddress.cpp | 19 ++++++ core/libs/tests/GuestUmtxOp.cpp | 60 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index bf4dc53cb..dda99c2ed 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -27,6 +27,7 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; constexpr int GUEST_CLOCK_REALTIME = 0; @@ -183,6 +184,21 @@ int UmtxWake(void* object, std::uint64_t count, bool isPrivate) { return 0; } +int UmtxNwakePrivate(void* object, std::uint64_t value) { + const auto count = static_cast(value); + if (count <= 0) { + return 0; + } + if (object == nullptr) { + return GUEST_EFAULT; + } + const auto* addresses = static_cast(object); + for (std::int32_t index = 0; index < count; ++index) { + WakeAddress(reinterpret_cast(addresses[index]), std::numeric_limits::max(), true); + } + return 0; +} + } // namespace extern "C" { @@ -252,6 +268,9 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_WAKE_PRIVATE: error = UmtxWake(object, value, operation == UMTX_OP_WAKE_PRIVATE); break; + case UMTX_OP_NWAKE_PRIVATE: + error = UmtxNwakePrivate(object, value); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index d67250976..48ec61d50 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,7 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; static constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -211,6 +212,63 @@ static void WakeWithoutWaitersSucceeds() { Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, INT_MAX) == 0); } +static void NwakeWakesEveryWaiterOnEachListedAddress() { + std::uint32_t first = 1; + std::uint64_t second = 1; + std::uint32_t unlisted = 1; + std::atomic started{0}; + std::atomic finished{0}; + std::atomic unlistedFinished{0}; + std::vector results(4, -2); + int unlistedResult = -2; + + std::vector waiters; + for (int i = 0; i < 3; ++i) { + waiters.emplace_back([&, i] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + ++finished; + }); + } + waiters.emplace_back([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + results[3] = Umtx(&second, UMTX_OP_WAIT, 1, nullptr, &failsafe); + ++finished; + }); + std::thread unlistedWaiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + ++unlistedFinished; + }); + AwaitCount(started, 5); + std::this_thread::sleep_for(SETTLE); + + void* addresses[] = {&first, nullptr, &second}; + Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 3) == 0); + AwaitCount(finished, 4); + std::this_thread::sleep_for(SETTLE); + Require(unlistedFinished.load() == 0); + + Require(Umtx(&unlisted, UMTX_OP_WAKE_PRIVATE, 1) == 0); + for (auto& waiter : waiters) waiter.join(); + unlistedWaiter.join(); + for (const int result : results) Require(result == 0); + Require(unlistedResult == 0); +} + +static void NwakeReadsOnlyTheCountedEntries() { + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffff00000000ULL) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffffULL) == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 1), GUEST_EFAULT)); + std::uint32_t word = 0; + void* addresses[] = {&word}; + Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -244,5 +302,7 @@ int main() { WakeReleasesAWaiterWithoutAValueChange(); WakeHonoursTheCountAndTheAddress(); WakeWithoutWaitersSucceeds(); + NwakeWakesEveryWaiterOnEachListedAddress(); + NwakeReadsOnlyTheCountedEntries(); RejectsBadObjectsAndOperations(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..734580bc7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -169,7 +169,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [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` and `UMTX_OP_WAKE_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. 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 +- [_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 - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded From 302a8774acac7e9e964229b57c2f99e85e8de0ad Mon Sep 17 00:00:00 2001 From: virp-mta Date: Sun, 11 Oct 2026 21:48:56 +0400 Subject: [PATCH 203/277] feat(libc): implement drand48, lrand48, mrand48 and srand48 --- core/libs/prx/libc/src/Random.cpp | 24 ++++++++++++++++++++++++ core/libs/tests/GuestRandomState.cpp | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index d53812833..49b48e16c 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -95,6 +95,8 @@ void SaveTypeWordLocked() { StoreWord(state.table, 0, static_cast(word)); } +std::uint16_t g_rand48Seed[3]{0x330e, 0xabcd, 0x1234}; + std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { if (seed == nullptr) throw std::invalid_argument(std::string(function) + ": null seed"); constexpr std::uint64_t Multiplier = 0x5deece66dull; @@ -124,6 +126,28 @@ std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { return static_cast(static_cast(NextRand48(seed, __func__) >> 16)); } +double APS5_VABI drand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__)), -48); +} + +std::int64_t APS5_VABI lrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 17); +} + +std::int64_t APS5_VABI mrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(static_cast(NextRand48(g_rand48Seed, __func__) >> 16)); +} + +void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { + std::lock_guard lock(g_randomLock); + g_rand48Seed[0] = 0x330e; + g_rand48Seed[1] = static_cast(static_cast(seedval)); + g_rand48Seed[2] = static_cast(static_cast(seedval) >> 16); +} + std::int64_t APS5_VABI random_nid_postfix() { std::lock_guard lock(g_randomLock); return static_cast(NextLocked()); diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 22267baa5..59b336db3 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -19,6 +19,10 @@ int* APS5_VABI __error_nid_postfix(); double APS5_VABI erand48_nid_postfix(std::uint16_t*); std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t*); std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t*); +double APS5_VABI drand48_nid_postfix(); +std::int64_t APS5_VABI lrand48_nid_postfix(); +std::int64_t APS5_VABI mrand48_nid_postfix(); +void APS5_VABI srand48_nid_postfix(std::int64_t); } static int failures = 0; @@ -63,6 +67,13 @@ static void CheckRand48() { const double value = erand48_nid_postfix(top); Require(value >= 0.0 && value < 1.0, "erand48 stays below 1"); + srand48_nid_postfix(0x1234abcd); + std::uint16_t expectedSeed[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 matches erand48 on initialized seed"); + Require(lrand48_nid_postfix() == nrand48_nid_postfix(expectedSeed), "lrand48 matches nrand48 on initialized seed"); + Require(mrand48_nid_postfix() == jrand48_nid_postfix(expectedSeed), "mrand48 matches jrand48 on initialized seed"); + Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 fourth value matches erand48"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; @@ -71,6 +82,13 @@ static void CheckRand48() { Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); Require(jrand48_nid_postfix(guest) == ::jrand48(host), "jrand48 matches the host"); } + ::srand48(1234567); + srand48_nid_postfix(1234567); + for (int round = 0; round < 32; ++round) { + Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); + Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); + Require(mrand48_nid_postfix() == ::mrand48(), "mrand48 matches the host"); + } #endif } From 896c95823696b99eff1dbb56793e68d22e8f43cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:49:46 +0200 Subject: [PATCH 204/277] docs(technical-debt): record that sceSaveDataDirNameSearch rejects sort key 4 Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index d2bd4dca5..592353e69 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -283,7 +283,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 -- [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 +- [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. 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 - [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 - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature From b3be79ffed536f07ed2760f8303bb3ad81842a17 Mon Sep 17 00:00:00 2001 From: krishnaraya Date: Sun, 11 Oct 2026 13:50:25 -0400 Subject: [PATCH 205/277] fix(agc): keep FORCE_DEGAMMA paired when ANISO_OVERRIDE rebuilds a sampler Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/src/ShaderResources.cpp | 4 ++-- .../tests/execution/ImageSampleForceDegamma.cpp | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..78affa393 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3144,9 +3144,9 @@ void ShaderResources::applyAnisoOverride() { if (!variant.has_value()) continue; const auto& words = *variant; if (context.samplerCache != nullptr && !SamplerCacheDisabled()) { - samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, false, source.integerBorder); + samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, true, source.integerBorder); } else { - auto resource = DecodeSamplerResource(words, source.unnormalized, false, source.integerBorder); + auto resource = DecodeSamplerResource(words, source.unnormalized, true, source.integerBorder); resource.compareEnable = source.compareEnable; samplers[index] = std::make_shared(context, resource); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index 965994506..e106d7cfa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -27,6 +27,8 @@ constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t AnisoOverride = 1u << 29u; constexpr std::uint32_t ForceDegamma = 1u << 20u; alignas(256) std::array Input{}; @@ -57,8 +59,8 @@ std::array TextureDescriptor(std::uint32_t format) { }; } -std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced) { - return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced, std::uint32_t filters) { + return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u) | filters, 0u}; } std::uint8_t Code8(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { @@ -92,12 +94,12 @@ float SrgbToLinear(std::uint8_t code) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); } -std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) { +std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced, std::uint32_t filters = 0u) { std::vector userData(20, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); const auto texture = TextureDescriptor(format); - const auto sampler = SamplerDescriptor(filter, forced); + const auto sampler = SamplerDescriptor(filter, forced, filters); 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); @@ -118,9 +120,9 @@ std::array Sample(AgcDriver::VulkanDevice& device, std::uint return Output; } -void RequireSame(const std::array& forced, const std::array& plain, const char* name) { +void RequireSame(const std::array& forced, const std::array& plain, const char* name, const char* first = "through FORCE_DEGAMMA", const char* second = "without it") { for (std::uint32_t index = 0; index < forced.size(); ++index) { - Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " through FORCE_DEGAMMA, " + std::to_string(plain[index]) + " without it"); + Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " " + first + ", " + std::to_string(plain[index]) + " " + second); } } @@ -161,6 +163,7 @@ int main() { 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"); std::puts("image sample force degamma tests passed"); return 0; From fdf8246f5e55500aec492ee789e00d51a2ec355f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:56:25 +0200 Subject: [PATCH 206/277] docs(architecture): describe APR and AMM command buffer queues --- docs/dev/ARCHITECTURE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f72550b6..96649a460 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -57,6 +57,14 @@ The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7e [`GuestPthreadCancelRecovery.cpp`](../../core/libs/tests/GuestPthreadCancelRecovery.cpp) checks pending requests before ordinary and timed semaphore acquisition, cleanup reentrancy and destructor ordering, enabled and disabled timed condition waits, canceled join output, and subsequent resource and thread reuse. Host atomic gates establish pending requests; acquiring the condition mutex after the worker releases it establishes wait entry without a scheduling sleep. Both `guest_pthread_cancel_recovery` and `guest_pthread_cancel_recovery_coarse` retain checks in Release and have a 20-second timeout, shorter than the 60-second guest waits. The latter sets `APS5_COARSE_TIMED_WAITS=1` to exercise the Windows fallback; Linux uses that condition-variable path by default. +## APR and AMM command buffers + +[`Apr.cpp`](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) executes APR and AMM command buffers. A buffer runs on the submitting thread until a `WaitOnAddress` or `WaitOnCounter` that is not yet satisfied; the rest of it, and every buffer submitted after it, is copied and runs on a worker thread, so a submitter that satisfies the wait after the submit returns does not hang. Buffers without such a wait still complete before the submit returns. + +APR and AMM each have their own queue and worker thread. Order is kept inside a queue; a wait in one does not hold up the other, because AMM `WriteAddress` commands advance the counters that APR buffers wait on. Submission ids are per queue, and `sceKernelAprWaitCommandBuffer` and `sceAmprAmmWaitCommandBufferCompletion` block until the queue has completed that id. A command that throws on a worker aborts the process. + +[`GuestAmpr.cpp`](../../core/libs/tests/GuestAmpr.cpp) checks both cases: a submit whose wait is satisfied afterwards returns and keeps buffer order, and an AMM buffer submitted after a waiting APR buffer still completes. + ## Graphics ```mermaid From fa726efe9490b7b0d0b374cf941b03d1d43fb18e Mon Sep 17 00:00:00 2001 From: goncalompontes Date: Sun, 11 Oct 2026 18:57:55 +0100 Subject: [PATCH 207/277] test(shader): cover the VOP3 interpolation result and source modifiers Extend the decode test to mul:4, div:2, -|v2|, -|v2| div:2, a p2 coordinate with neg and a non-zero attribute channel, a clamped p2, and the mov form with clamp and mul:2. All 18 accepted words are the assembler output (llvm-mc -mcpu=gfx1010). --- core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp index b3a4aca23..f362f67d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -44,6 +44,14 @@ int main() { {{0xd6000005u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, {{0xd6008005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, {{0xd6000005u, 0x0a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, + {{0xd6000005u, 0x12020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:4", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 2u}, + {{0xd6000005u, 0x1a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 3u}, + {{0xd6000205u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 0u}, + {{0xd6000205u, 0x5a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 3u}, + {{0xd6010005u, 0x42020481u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, -v2, attr1.z", RdnaOperandKind::VectorRegister, 2u, 1u, 2u, false, true, false, 0u}, + {{0xd6018205u, 0x02020400u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, |v2|, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, true, 0u}, + {{0xd6028005u, 0x02000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x clamp", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, true, 0u}, + {{0xd6020005u, 0x0a000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x mul:2", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, false, 1u}, }; const std::vector refused{ {{0xd6000005u, 0x02000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, From 07af520c1c19a67ce19bffff6cc8f6807db38af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:58:11 +0200 Subject: [PATCH 208/277] docs(technical-debt): move the vieBRwlh1Lw entry off the end of Functional Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3827dbf51..53e47c93f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -322,6 +322,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayInitialize](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` - [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [Shader header copies](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown @@ -597,4 +598,3 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` From d33dad00b850603d5ce40273c8c89bff24f7ba62 Mon Sep 17 00:00:00 2001 From: gookie <36005381+gookie-dev@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:58:13 +0200 Subject: [PATCH 209/277] fix(libSceXml): add an export-free libSceXml for titles that list it PPSA19546 (Cookie Clicker) and PPSA34959 (The King is Watching) list libSceXml.prx among their needed libraries but import nothing from it. AnyPS5 builds no such library, so on Linux the dynamic loader stops both before they run: ./eboot.elf: error while loading shared libraries: libSceXml.prx: cannot open shared object file: No such file or directory On Windows the start-up stub has skipped a missing library that nothing imports from since #3129; the Linux loader resolves DT_NEEDED itself and cannot skip one. libSceXml now exists with no exports, like libSceAgcVsh and libSceAudio, so it loads on both. A title that imports from it still stops at the missing import. Both titles now load and stop at their next missing import: setSpecialFloatFormatType (PPSA19546, #3575) and sceHttpsFreeCaList (PPSA34959, #3497). --- core/libs/prx/libSceXml/Export.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 core/libs/prx/libSceXml/Export.cpp diff --git a/core/libs/prx/libSceXml/Export.cpp b/core/libs/prx/libSceXml/Export.cpp new file mode 100644 index 000000000..fb34206c2 --- /dev/null +++ b/core/libs/prx/libSceXml/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} From c61f07409f8c83307bc475447a4e9c5e7c98445f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 19:58:44 +0200 Subject: [PATCH 210/277] test(shader): cover clamp alone and inf/NaN/fraction rows in v_cvt_i16_f16 output modifiers Co-Authored-By: Claude --- .../tests/execution/VCvtI16F16OutputModifiers.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp index b4517eaac..d7a66c2b4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -19,7 +19,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 16; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 4; -constexpr std::uint32_t RowCount = 12; +constexpr std::uint32_t RowCount = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -39,6 +39,7 @@ constexpr Variant Variants[] = { {"mul:2", 0, 1}, {"mul:4", 0, 2}, {"div:2", 0, 3}, + {"clamp", 1, 0}, {"clamp mul:2", 1, 1}, }; @@ -55,12 +56,17 @@ constexpr std::uint32_t Rows[RowCount] = { 0x00007e00u, 0xbeef3c00u, 0xbeefc100u, + 0x00007c00u, + 0x0000fc00u, + 0x00007c01u, + 0x00003e00u, }; constexpr std::uint32_t Expected[RowCount] = { 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, + 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { From eedf7a267124dfa46c9771cda3bb08198ca6ca5b Mon Sep 17 00:00:00 2001 From: Chad Date: Sun, 11 Oct 2026 13:23:50 -0500 Subject: [PATCH 211/277] feat(savedata): implement sceSaveDataConvert PPSA28416 calls it on its existing save at boot (after sceSaveDataDirNameSearch finds it) and aborted on the missing implementation. The call takes one parameter block: user id at +0, the directory name pointer at +8. Host saves have no size limit, so the conversion completes at once: an existing save returns OK, a missing one NOT_FOUND, an invalid name PARAMETER. The title polls sceSaveDataGetEventResult once afterwards and continues on 'no event'. --- core/libs/SceTypes.hpp | 7 ++ .../prx/libSceSaveData.native/CMakeLists.txt | 9 +++ .../libs/prx/libSceSaveData.native/Export.cpp | 25 ++++++- core/libs/tests/GuestSaveDataConvert.cpp | 73 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestSaveDataConvert.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..b288e6227 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1563,6 +1563,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 3b596955e..6b03a2c86 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..aeaca60c5 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -740,9 +740,28 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataGetConvertProgress() { diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 000000000..8df9b84da --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataInitialize3(const void*); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..aaffeaec6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -125,7 +125,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title. The 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [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 - [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 - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached From f9fd0f125c357cb5558c6ac90cc5e689dcac7a9f Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 18:28:02 +0000 Subject: [PATCH 212/277] fix(recompiler): give out-of-range relative vector accesses the measured semantics M0 is unsigned, so an index at or past the window reads v0 and drops its write, v_swaprel_b32 writes neither side when either is out of range, and the window array starts zeroed like unwritten SSA registers; the window load counts as a lane source so a wave64 program's second half keeps its own registers. agc_driver_relative_indexing runs in wave32 and wave64 and pins the rows measured on gfx1035 in the review of #3856, and the TechnicalDebt entry states the measured behaviour, the window's end at the last named register and the sizes. Co-Authored-By: Claude Fable 5.1 --- .../tests/execution/RelativeIndexing.cpp | 86 +++++++------------ .../SpirvBackend/src/SpirvAnalysis.cpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 4 +- .../Translation/TranslationContext.hpp | 2 +- .../src/ControlFlowInstructions.cpp | 62 +++++-------- docs/dev/TechnicalDebt.md | 2 +- 6 files changed, 61 insertions(+), 96 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp index 8b2ab8f12..ca3055aac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp @@ -17,14 +17,15 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t MaxThreads = 64; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 16; -alignas(256) std::array Input{}; -alignas(256) std::array Output{}; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t Checked = 17; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ - 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060087, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, 0x7e240280, 0x7e260280, 0x7e280280, 0xbf8c3f70, 0x7e3c0304, 0xd525001f, 0x00010304, 0xd5250020, 0x00010504, 0xd5250021, 0x00010704, 0xd5250022, 0x00010904, 0xd5250023, 0x00010b04, 0xd5250024, 0x00010d04, 0xd5250025, 0x00010f04, 0xbefc0382, @@ -34,44 +35,19 @@ alignas(256) constexpr std::array Code{ 0x0000011f, 0x7e280323, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03, 0xe070100c, 0x80010d03, 0xe0701010, 0x80010e03, 0xe0701014, 0x80010f03, 0xe0701018, 0x80011003, 0xe070101c, 0x80011103, 0xe0701020, 0x80011203, 0xe0701024, 0x80011303, 0xe0701028, 0x80011403, - 0xbf810000, + 0xbefc03c1, 0x7e2a8720, 0xe070102c, 0x80011503, 0x7e428504, 0xe0701030, 0x80012003, 0x7e468922, + 0xe0701034, 0x80012203, 0xe0701038, 0x80012103, 0xbefc03ff, 0x000103ff, 0x7e3ed120, 0xe070103c, + 0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0xbf810000, }; -constexpr std::uint32_t Expected[32][11] = { - {0x00001002u, 0x00001002u, 0x00001001u, 0x00001030u, 0x00001020u, 0x00001000u, 0x00001007u, 0x00001006u, 0x00001007u, 0x00001005u, 0x00001005u}, - {0x00002002u, 0x00002002u, 0x00002001u, 0x00002030u, 0x00002020u, 0x00002000u, 0x00002007u, 0x00002006u, 0x00002007u, 0x00002005u, 0x00002005u}, - {0x00003002u, 0x00003002u, 0x00003001u, 0x00003030u, 0x00003020u, 0x00003000u, 0x00003007u, 0x00003006u, 0x00003007u, 0x00003005u, 0x00003005u}, - {0x00004002u, 0x00004002u, 0x00004001u, 0x00004030u, 0x00004020u, 0x00004000u, 0x00004007u, 0x00004006u, 0x00004007u, 0x00004005u, 0x00004005u}, - {0x00005002u, 0x00005002u, 0x00005001u, 0x00005030u, 0x00005020u, 0x00005000u, 0x00005007u, 0x00005006u, 0x00005007u, 0x00005005u, 0x00005005u}, - {0x00006002u, 0x00006002u, 0x00006001u, 0x00006030u, 0x00006020u, 0x00006000u, 0x00006007u, 0x00006006u, 0x00006007u, 0x00006005u, 0x00006005u}, - {0x00007002u, 0x00007002u, 0x00007001u, 0x00007030u, 0x00007020u, 0x00007000u, 0x00007007u, 0x00007006u, 0x00007007u, 0x00007005u, 0x00007005u}, - {0x00008002u, 0x00008002u, 0x00008001u, 0x00008030u, 0x00008020u, 0x00008000u, 0x00008007u, 0x00008006u, 0x00008007u, 0x00008005u, 0x00008005u}, - {0x00009002u, 0x00009002u, 0x00009001u, 0x00009030u, 0x00009020u, 0x00009000u, 0x00009007u, 0x00009006u, 0x00009007u, 0x00009005u, 0x00009005u}, - {0x0000a002u, 0x0000a002u, 0x0000a001u, 0x0000a030u, 0x0000a020u, 0x0000a000u, 0x0000a007u, 0x0000a006u, 0x0000a007u, 0x0000a005u, 0x0000a005u}, - {0x0000b002u, 0x0000b002u, 0x0000b001u, 0x0000b030u, 0x0000b020u, 0x0000b000u, 0x0000b007u, 0x0000b006u, 0x0000b007u, 0x0000b005u, 0x0000b005u}, - {0x0000c002u, 0x0000c002u, 0x0000c001u, 0x0000c030u, 0x0000c020u, 0x0000c000u, 0x0000c007u, 0x0000c006u, 0x0000c007u, 0x0000c005u, 0x0000c005u}, - {0x0000d002u, 0x0000d002u, 0x0000d001u, 0x0000d030u, 0x0000d020u, 0x0000d000u, 0x0000d007u, 0x0000d006u, 0x0000d007u, 0x0000d005u, 0x0000d005u}, - {0x0000e002u, 0x0000e002u, 0x0000e001u, 0x0000e030u, 0x0000e020u, 0x0000e000u, 0x0000e007u, 0x0000e006u, 0x0000e007u, 0x0000e005u, 0x0000e005u}, - {0x0000f002u, 0x0000f002u, 0x0000f001u, 0x0000f030u, 0x0000f020u, 0x0000f000u, 0x0000f007u, 0x0000f006u, 0x0000f007u, 0x0000f005u, 0x0000f005u}, - {0x00010002u, 0x00010002u, 0x00010001u, 0x00010030u, 0x00010020u, 0x00010000u, 0x00010007u, 0x00010006u, 0x00010007u, 0x00010005u, 0x00010005u}, - {0x00011002u, 0x00011002u, 0x00011001u, 0x00011030u, 0x00011020u, 0x00011000u, 0x00011007u, 0x00011006u, 0x00011007u, 0x00011005u, 0x00011005u}, - {0x00012002u, 0x00012002u, 0x00012001u, 0x00012030u, 0x00012020u, 0x00012000u, 0x00012007u, 0x00012006u, 0x00012007u, 0x00012005u, 0x00012005u}, - {0x00013002u, 0x00013002u, 0x00013001u, 0x00013030u, 0x00013020u, 0x00013000u, 0x00013007u, 0x00013006u, 0x00013007u, 0x00013005u, 0x00013005u}, - {0x00014002u, 0x00014002u, 0x00014001u, 0x00014030u, 0x00014020u, 0x00014000u, 0x00014007u, 0x00014006u, 0x00014007u, 0x00014005u, 0x00014005u}, - {0x00015002u, 0x00015002u, 0x00015001u, 0x00015030u, 0x00015020u, 0x00015000u, 0x00015007u, 0x00015006u, 0x00015007u, 0x00015005u, 0x00015005u}, - {0x00016002u, 0x00016002u, 0x00016001u, 0x00016030u, 0x00016020u, 0x00016000u, 0x00016007u, 0x00016006u, 0x00016007u, 0x00016005u, 0x00016005u}, - {0x00017002u, 0x00017002u, 0x00017001u, 0x00017030u, 0x00017020u, 0x00017000u, 0x00017007u, 0x00017006u, 0x00017007u, 0x00017005u, 0x00017005u}, - {0x00018002u, 0x00018002u, 0x00018001u, 0x00018030u, 0x00018020u, 0x00018000u, 0x00018007u, 0x00018006u, 0x00018007u, 0x00018005u, 0x00018005u}, - {0x00019002u, 0x00019002u, 0x00019001u, 0x00019030u, 0x00019020u, 0x00019000u, 0x00019007u, 0x00019006u, 0x00019007u, 0x00019005u, 0x00019005u}, - {0x0001a002u, 0x0001a002u, 0x0001a001u, 0x0001a030u, 0x0001a020u, 0x0001a000u, 0x0001a007u, 0x0001a006u, 0x0001a007u, 0x0001a005u, 0x0001a005u}, - {0x0001b002u, 0x0001b002u, 0x0001b001u, 0x0001b030u, 0x0001b020u, 0x0001b000u, 0x0001b007u, 0x0001b006u, 0x0001b007u, 0x0001b005u, 0x0001b005u}, - {0x0001c002u, 0x0001c002u, 0x0001c001u, 0x0001c030u, 0x0001c020u, 0x0001c000u, 0x0001c007u, 0x0001c006u, 0x0001c007u, 0x0001c005u, 0x0001c005u}, - {0x0001d002u, 0x0001d002u, 0x0001d001u, 0x0001d030u, 0x0001d020u, 0x0001d000u, 0x0001d007u, 0x0001d006u, 0x0001d007u, 0x0001d005u, 0x0001d005u}, - {0x0001e002u, 0x0001e002u, 0x0001e001u, 0x0001e030u, 0x0001e020u, 0x0001e000u, 0x0001e007u, 0x0001e006u, 0x0001e007u, 0x0001e005u, 0x0001e005u}, - {0x0001f002u, 0x0001f002u, 0x0001f001u, 0x0001f030u, 0x0001f020u, 0x0001f000u, 0x0001f007u, 0x0001f006u, 0x0001f007u, 0x0001f005u, 0x0001f005u}, - {0x00020002u, 0x00020002u, 0x00020001u, 0x00020030u, 0x00020020u, 0x00020000u, 0x00020007u, 0x00020006u, 0x00020007u, 0x00020005u, 0x00020005u}, -}; -constexpr const char* Names[11] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_out_of_range"}; +constexpr const char* Names[Checked] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_dst", + "movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range"}; + +std::array ExpectedFor(std::uint32_t tid) { + const std::uint32_t x = 0x1000u * (tid + 1u); + return {x + 2u, x + 2u, x + 1u, x + 0x30u, x + 0x20u, x, x + 7u, x + 6u, x + 7u, x + 5u, x + 5u, + tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid}; +} void Fill(std::uint32_t tid, std::uint32_t* words) { words[0] = 0x1000u * (tid + 1u); @@ -88,12 +64,12 @@ std::string Hex(std::uint32_t value) { return text; } -void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("relative indexing: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +void Expect(std::uint32_t waveSize, std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("relative indexing: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } -void Run(AgcDriver::VulkanDevice& device) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); @@ -102,10 +78,10 @@ void Run(AgcDriver::VulkanDevice& device) { std::copy(output.begin(), output.end(), userData.begin() + 4); const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + 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}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; @@ -115,11 +91,11 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { - for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const std::uint32_t* in = &Input[tid * Inputs]; +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t index = 0; index < 11; ++index) Expect(tid, out[index], Expected[tid][index], Names[index]); + const auto expected = ExpectedFor(tid); + for (std::uint32_t index = 0; index < Checked; ++index) Expect(waveSize, tid, out[index], expected[index], Names[index]); } } @@ -129,8 +105,10 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); - Check(); + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); std::puts("relative indexing tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 0014b0bfb..4742b649a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -48,6 +48,7 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::WriteLane: case IrOpcode::DataAppend: case IrOpcode::DataConsume: + case IrOpcode::LoadVectorWindow: return true; case IrOpcode::GetBuiltin: { const auto* kind = value.Argument(0)->Resolve(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f2bd0fde9..6124580ce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1000,8 +1000,10 @@ void EmitProgram(SpirvEmitterState& state) { } } if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, window)); + const auto zero = state.module.Constant(spv::OpConstantNull, array); for (std::uint32_t half = 0; half < state.laneCount; half++) { - state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, window), state.vectorWindowVariable.at(half), spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, array), state.vectorWindowVariable.at(half), spv::StorageClassFunction, zero); } } if (state.pixelValidMaskVariable != 0u) { diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a4d392a4..5de5c81a3 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -319,7 +319,7 @@ private: void vMovrelsdB32(const RdnaInstruction& inst, bool split, bool swap); void vSwapB32(const RdnaInstruction& inst); IrU32 readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name); - void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name); + void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable = nullptr); void vReadfirstlaneB32(const RdnaInstruction& inst); void vReadlaneB32(const RdnaInstruction& inst); void vWritelaneB32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 8bf064fb1..b1ac114b6 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -293,62 +293,45 @@ void TranslationContext::sWqm(const RdnaInstruction& inst, bool wide) { ir.SetScc(ir.INotEqual(low.Value(), ir.Constant(0u))); } -// M0 holds a uniform register offset. Vector registers are SSA values with fixed indices, so an indexed -// access becomes a select over every register the program references at or past the base. struct RelativeWindowAccess { IrValue* index = nullptr; IrValue* inRange = nullptr; }; -RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset) { +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name) { const auto& metadata = program.Metadata(); - if (metadata.vectorWindowSize == 0u) return {}; - if (reg < metadata.vectorWindowBase) throw std::runtime_error("relative vector access below the relative-addressing window"); - IrValue& raw = ir.IAdd(ir.Constant(reg - metadata.vectorWindowBase), offset); - IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&raw, &ir.Constant(metadata.vectorWindowSize - 1u)}); - return {&index, &ir.ULessThan(raw, ir.Constant(metadata.vectorWindowSize))}; + if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window"); + const auto first = reg - metadata.vectorWindowBase; + IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)}); + return {&index, &ir.ULessThan(offset, ir.Constant(metadata.vectorWindowSize - first))}; } IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name) { if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } - if (const auto access = relativeWindowAccess(ir, program, base.reg, offset); access.index != nullptr) { - IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); - return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.Constant(0u)))); - } - IrU32 result(ir.GetVectorReg(static_cast(base.reg))); - for (std::uint32_t reg = base.reg + 1u; reg < currentVectorLimit; ++reg) { - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); - result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); - } - return applyBitSourceModifiers(base, result); + const auto access = relativeWindowAccess(ir, program, base.reg, offset, name); + IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast(0))))); } -void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name) { +void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable) { const RdnaOperand& destination = base; if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } - if (const auto access = relativeWindowAccess(ir, program, destination.reg, offset); access.index != nullptr) { - IrValue* index = access.index; - IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); - IrValue* merged = nullptr; - if (destination.dpp) { - const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; - merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); - } else { - merged = &ir.Select(ir.GetExec(), value.Value(), previous); - } - (void)ir.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &ir.Select(*access.inRange, *merged, previous)}); - return; - } - for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { - RdnaOperand target = destination; - target.reg = reg; - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - destination.reg)); - writeRawU32(target, IrU32(ir.Select(hit, value.Value(), ir.GetVectorReg(static_cast(reg))))); + const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name); + IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else { + merged = &ir.Select(ir.GetExec(), value.Value(), previous); } + IrValue* written = access.inRange; + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + (void)ir.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {access.index, &ir.Select(*written, *merged, previous)}); } void TranslationContext::vMovrelsB32(const RdnaInstruction& inst) { @@ -371,8 +354,9 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b return; } const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name); - writeRelativeVector(inst.destination, destinationOffset, value, name); - writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name); + IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name).inRange); + writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange); + writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange); } void TranslationContext::vSwapB32(const RdnaInstruction& inst) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 719999491..dba9b5cdd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -577,7 +577,7 @@ Throughout the project, every function at every stage either **does exactly what - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to its last allocated register in a 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. An index past the window reads 0 and its write is dropped, as the RDNA ISA describes for out-of-range VGPR indexing, where the earlier select chain read the base register and dropped the write; neither is measured on the console. Killzone: Liberation's PSP vertex program (PPSA11142) went from 649,515 to the recompiled size noted in its pull request with this. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. 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. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws - [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. From 1462312ebf9dd620d38710827a12166acbae1f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 20:28:50 +0200 Subject: [PATCH 213/277] docs(savedata): record the inferred sceSaveDataConvert contract --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index aaffeaec6..37f478e76 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -126,6 +126,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [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 +- [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 - [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 - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached From 4eb3340dcb851de430dd7a98a8ec5ebf737e480f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:31:52 +0200 Subject: [PATCH 214/277] fix(rudp): throw from sceRudpGetMaxSegmentSize and justify the success of the pump calls Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceRudp/Export.cpp | 10 ++-------- core/libs/tests/GuestRudpPump.cpp | 26 ++++++++++---------------- docs/dev/TechnicalDebt.md | 4 +--- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 06a56008a..a10f455bd 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -258,14 +258,8 @@ int APS5_VABI sceRudpNetReceived(void) { return 0; } -int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { - if (size == nullptr) { - return RUDP_ERROR_INVALID_ARGUMENT; - } - std::lock_guard lk(g_mutex); - if (!g_inited) { - return RUDP_ERROR_NOT_INITIALIZED; - } +int APS5_VABI sceRudpGetMaxSegmentSize(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 999a4cf13..36cf63cb4 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -11,7 +11,7 @@ int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpTerminate(); int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); -int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); +int APS5_VABI sceRudpGetMaxSegmentSize(); int APS5_VABI sceRudpNetReceived(); } @@ -25,29 +25,23 @@ static void Require(bool value, int line) { int main() { constexpr int notInitialized = static_cast(0x80770001); - constexpr int invalidArgument = static_cast(0x80770004); - std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents() == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); - CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); - CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); - CHECK(segmentSize == 0x5a5a); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); CHECK(sceRudpProcessEvents() == 0); CHECK(sceRudpNetFlush() == 0); - CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); - CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); - CHECK(segmentSize == 0x5a5a); CHECK(sceRudpTerminate() == 0); - try { - sceRudpNetReceived(); - } catch (const std::runtime_error& error) { - CHECK(std::string(error.what()) == "sceRudpNetReceived not implemented"); - return 0; + for (const auto call : {&sceRudpNetReceived, &sceRudpGetMaxSegmentSize}) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error&) { + threw = true; + } + CHECK(threw); } - std::fprintf(stderr, "sceRudpNetReceived returned instead of reporting that it is not implemented\n"); - return 1; + return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b32c54ad8..be44034be 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -39,6 +39,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [sceRudpProcessEvents and sceRudpNetFlush](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does - [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication @@ -597,6 +598,3 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- PPSA03490 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceRudpNetReceived](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) -- [sceRudpProcessEvents and sceRudpNetFlush](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, so with nothing to process or send they return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown -- [sceRudpGetMaxSegmentSize](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - PPSA03490 passes a pointer to a 16-bit value it has set to 0 and treats a nonzero result as failure. The real segment size is unknown, so nothing is written and the title keeps 0; it returns `RUDP_ERROR_INVALID_ARGUMENT` for a null pointer, `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit` and 0 after it From b1104a2c65a81b939d50da2a055c9b2236e641ec Mon Sep 17 00:00:00 2001 From: Jordan Hans Date: Sun, 11 Oct 2026 18:33:48 +0000 Subject: [PATCH 215/277] fix(libSceSysmodule): recognize the libSceAgc internal module id --- core/libs/prx/libSceSysmodule/ModuleTable.hpp | 1 + core/libs/tests/GuestSysmodule.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 4840450be..14642ee07 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -275,6 +275,7 @@ inline const std::unordered_map kModuleTable = { {0x8000008c, "libSceHttp2"}, {0x8000008d, "libSceNpGameIntent"}, {0x8000008f, "libSceNpWebApi2"}, + {0x80000094, "libSceAgc"}, {0x80000098, "libScePosixForWebKit"}, {0x8000009a, "libSceNpRemotePlaySessionSignaling"}, {0x800000aa, "libicu"}, diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4a63bbd37..5b664456a 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -40,6 +40,7 @@ constexpr std::uint16_t kFiberModuleId = 0x0006; constexpr std::uint16_t kUltModuleId = 0x0007; constexpr int kModuleUnloaded = static_cast(0x805A1001); constexpr std::uint16_t kTrophy2ModuleId = 0x0110; +constexpr std::uint32_t kAgcModuleId = 0x80000094; #ifdef __linux__ struct Mapping { @@ -124,6 +125,13 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == 0); Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == kModuleUnloaded); + + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); #ifdef __linux__ CheckUnwindInfo(); #endif From d9afe1078eec00a403ee1763c3fd8497d1013013 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 07:35:46 +1300 Subject: [PATCH 216/277] test(agc): enter whole quad mode in the implicit-LOD 1D sample test A pixel wave starts with only its covered pixels in EXEC, so a shader that takes an implicit LOD has to run s_wqm first or the helper lanes of a partly covered quad hold no coordinate. The test's pixel shader did not, which goes unnoticed while its one triangle covers every quad whole. llvmpipe clips that triangle at twice the viewport into three, and in the quads on the two inner edges the helpers' zero coordinate gave the last mip instead of the expected one. The shader now starts with s_wqm_b64 exec, exec, as a compiled pixel shader that samples with an implicit LOD does. --- .../tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp index cc39b8471..28a89a1f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp @@ -32,8 +32,8 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; -alignas(256) constexpr std::array PixelCode{ - 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, +alignas(256) constexpr std::array PixelCode{ + 0xbefe0a7e, 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, }; alignas(256) constexpr std::array BiasPixelCode{ From 876c4ecacfc6dd96f06bde3b9fe3147bec36f6c8 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 18:36:09 +0000 Subject: [PATCH 217/277] test(recompiler): pin that an unwritten window register reads 0 Two more agc_driver_relative_indexing rows read a register of the window the program never wrote, directly and through v_movrels_b32, and expect 0, which the zeroed window array gives like an unwritten SSA register did; the TechnicalDebt entry says the console's value is undefined. Co-Authored-By: Claude Fable 5.1 --- .../tests/execution/RelativeIndexing.cpp | 12 +++++++----- docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp index ca3055aac..225501329 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp @@ -20,11 +20,11 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t MaxThreads = 64; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 32; -constexpr std::uint32_t Checked = 17; +constexpr std::uint32_t Checked = 19; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060087, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, 0x7e240280, 0x7e260280, 0x7e280280, 0xbf8c3f70, 0x7e3c0304, 0xd525001f, 0x00010304, 0xd5250020, 0x00010504, 0xd5250021, 0x00010704, 0xd5250022, @@ -37,16 +37,18 @@ alignas(256) constexpr std::array Code{ 0xe070101c, 0x80011103, 0xe0701020, 0x80011203, 0xe0701024, 0x80011303, 0xe0701028, 0x80011403, 0xbefc03c1, 0x7e2a8720, 0xe070102c, 0x80011503, 0x7e428504, 0xe0701030, 0x80012003, 0x7e468922, 0xe0701034, 0x80012203, 0xe0701038, 0x80012103, 0xbefc03ff, 0x000103ff, 0x7e3ed120, 0xe070103c, - 0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0xbf810000, + 0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0x7e2c0327, 0xe0701044, 0x80011603, 0xbefc0388, + 0x7e2e871e, 0xe0701048, 0x80011703, 0xbf810000, }; constexpr const char* Names[Checked] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_dst", - "movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range"}; + "movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range", + "unwritten_direct", "unwritten_relative"}; std::array ExpectedFor(std::uint32_t tid) { const std::uint32_t x = 0x1000u * (tid + 1u); return {x + 2u, x + 2u, x + 1u, x + 0x30u, x + 0x20u, x, x + 7u, x + 6u, x + 7u, x + 5u, x + 5u, - tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid}; + tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid, 0u, 0u}; } void Fill(std::uint32_t tid, std::uint32_t* words) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dba9b5cdd..f55bdf9e4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -577,7 +577,7 @@ Throughout the project, every function at every stage either **does exactly what - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws - [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. From cf2ca2b1035fa03893f979736319648305464b4c Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 03:41:02 +0900 Subject: [PATCH 218/277] fix(libc): list stack return address candidates in Windows backtrace reports The abort() and UCRT invalid-parameter reports capture the stack with RtlCaptureStackBackTrace, which stops at the first frame without unwind data (guest code, or our libraries built without asynchronous unwind tables), so they often end two or three frames in, before any caller. The fault report already scans the stack for code addresses; that scan is now a helper the backtrace reports use as well. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 6 +++++ .../src/specifics/windows/CrashReport.cpp | 26 ++++++++++++------- core/libs/tests/WindowsBacktraceReport.cpp | 12 +++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/WindowsBacktraceReport.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ddfe85664..e5ce124f6 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -569,5 +569,11 @@ if(MINGW) set_target_properties(windows_exception_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME windows_exceptions COMMAND windows_exception_tests) set_tests_properties(windows_exceptions PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(windows_backtrace_report_tests ${LIBS_INCLUDE_DIR}/tests/WindowsBacktraceReport.cpp) + target_link_libraries(windows_backtrace_report_tests PRIVATE libc) + add_test(NAME windows_backtrace_report COMMAND windows_backtrace_report_tests) + set_tests_properties(windows_backtrace_report PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*stack return address candidates:[^~]*rsp[+]0x") endif() endif() diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 099d6392a..b85ba8b1a 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -238,6 +238,20 @@ bool HandleSse4a(EXCEPTION_POINTERS* info) { return true; } +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span) { + char line[256]; + Report(" stack return address candidates:\n"); + int printed = 0; + for (std::uint64_t slot = rsp; printed < limit && slot < rsp + span; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (!IsExecutable(value)) continue; + DescribeAddress(value, line, sizeof(line)); + Report(" [rsp+0x%llx] %s\n", static_cast(slot - rsp), line); + ++printed; + } +} + LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; const auto* fault = info->ExceptionRecord; @@ -296,16 +310,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { if (next <= frame) break; frame = next; } - Report(" stack return address candidates:\n"); - int printed = 0; - for (std::uint64_t slot = context->Rsp; printed < 16 && slot < context->Rsp + 0x2000; slot += 8) { - if (!IsReadable(slot)) break; - const auto value = *reinterpret_cast(slot); - if (!IsExecutable(value)) continue; - DescribeAddress(value, line, sizeof(line)); - Report(" [rsp+0x%llx] %s\n", static_cast(slot - context->Rsp), line); - ++printed; - } + ReportStackCandidates(context->Rsp, 16, 0x2000); if (context->Rip == 0 || !IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. @@ -444,6 +449,7 @@ void ReportBacktrace(const char* what, bool reportThreads) { DescribeAddress(reinterpret_cast(frames[i]), line, sizeof(line)); Report(" #%u %s\n", static_cast(i), line); } + ReportStackCandidates(reinterpret_cast(__builtin_frame_address(0)), 32, 0x4000); if (reportThreads) ReportAllThreads(); std::fflush(stderr); } diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp new file mode 100644 index 000000000..a5d93edc4 --- /dev/null +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -0,0 +1,12 @@ +#include +#include +#include "../prx/libc/include/general/VabiMacros.hpp" + +extern "C" int* APS5_VABI __error_nid_postfix(); + +int main() { + if (__error_nid_postfix() == nullptr) return 1; + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + std::fputs("aborting\n", stderr); + std::abort(); +} From 10740f85f978270d5d9447c2ee3852b3fd3fde9c Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Mon, 12 Oct 2026 03:42:52 +0900 Subject: [PATCH 219/277] refactor(libc): define the stack candidate helper beside ReportBacktrace Co-Authored-By: Claude Opus 5.5 --- .../src/specifics/windows/CrashReport.cpp | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index b85ba8b1a..53d651430 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -31,6 +31,7 @@ void Report(const char* format, ...) { } void ReportAllThreads(); +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span); void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; @@ -238,20 +239,6 @@ bool HandleSse4a(EXCEPTION_POINTERS* info) { return true; } -void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span) { - char line[256]; - Report(" stack return address candidates:\n"); - int printed = 0; - for (std::uint64_t slot = rsp; printed < limit && slot < rsp + span; slot += 8) { - if (!IsReadable(slot)) break; - const auto value = *reinterpret_cast(slot); - if (!IsExecutable(value)) continue; - DescribeAddress(value, line, sizeof(line)); - Report(" [rsp+0x%llx] %s\n", static_cast(slot - rsp), line); - ++printed; - } -} - LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; const auto* fault = info->ExceptionRecord; @@ -438,6 +425,20 @@ DWORD WINAPI HangWatchdog(LPVOID param) { return 0; } +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span) { + char line[256]; + Report(" stack return address candidates:\n"); + int printed = 0; + for (std::uint64_t slot = rsp; printed < limit && slot < rsp + span; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (!IsExecutable(value)) continue; + DescribeAddress(value, line, sizeof(line)); + Report(" [rsp+0x%llx] %s\n", static_cast(slot - rsp), line); + ++printed; + } +} + // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail // (0xc0000409); both are reported with the calling thread's stack first. void ReportBacktrace(const char* what, bool reportThreads) { From 5740cd6c20433a10aa142c7cc08601964f312820 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:52:58 +0400 Subject: [PATCH 220/277] fix(build): separate UTF-8 and control-flow test registration --- core/relinker/CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d72b4764d..3bf18fd70 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -163,6 +163,13 @@ if(BUILD_TESTING) target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) add_test(NAME buffer_bounds COMMAND buffer_bounds_tests) + add_test_executable(param_json_utf8_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) + target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) + add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) + set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp @@ -393,13 +400,6 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) - add_test_executable(param_json_utf8_tests - ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) - target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) - add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) - set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) - add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp From 3742fa136e01442c81f15075fd733c8f90cf9cd9 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:11:42 +0400 Subject: [PATCH 221/277] test(relinker): cover extra block bounds and header address overflow --- core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp index c2059ece9..3cf152c37 100644 --- a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -25,7 +25,7 @@ int main() { Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 6, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; std::vector output(0x1100, 0xA5); Require(builder.WriteLayout(output, valid) == 6); - for (int kind = 0; kind < 9; ++kind) { + for (int kind = 0; kind < 11; ++kind) { auto request = valid; switch (kind) { case 0: request.PhEntSize = 55; break; @@ -37,6 +37,8 @@ int main() { case 6: request.DynamicSegmentOffset = maximum; break; case 7: request.InterpOffset = maximum; break; case 8: request.ExtraBlockOffset = maximum; break; + case 9: request.ExtraBlockSize = 0x1000; break; + case 10: request.PhOff = 0xF00; request.ExtraBlockVaddr = maximum - 0x1fff; break; } std::vector bytes(0x1100, 0xA5); const auto before = bytes; @@ -44,6 +46,6 @@ int main() { try { builder.WriteLayout(bytes, request); } catch (const std::exception&) { rejected = true; } Require(rejected && bytes == before); } - std::cout << "12 program layout cases passed\n"; + std::cout << "14 program layout cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } From 4a419397cb896c88b8779e74738e00c043890378 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:57:08 +0000 Subject: [PATCH 222/277] fix(libkernel_sys): translate host errors to guest errno numbers GuestError passed host error values up to 34 through and reported every other value as EIO, so readlink failed with EIO for a name that is too long or a symbolic link loop, and a host EAGAIN would have been reported as the guest's EDEADLK. The values whose numbering differs are now mapped to FreeBSD's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel_sys/Export.cpp | 15 ++++++++++++++- core/libs/tests/GuestKernelSys.cpp | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index f477e9aa2..12b30f0b5 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -36,6 +36,12 @@ constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEagain = 35; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int GuestEnotempty = 66; +constexpr int GuestEoverflow = 84; std::atomic creationMask{022}; @@ -45,7 +51,14 @@ std::int64_t Fail(int error) { } int GuestError(const std::error_code& error) { - const int value = error.default_error_condition().value(); + const auto condition = error.default_error_condition(); + if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; + if (condition == std::errc::resource_deadlock_would_occur) return GuestEdeadlk; + if (condition == std::errc::too_many_symbolic_link_levels) return GuestEloop; + if (condition == std::errc::filename_too_long) return GuestEnametoolong; + if (condition == std::errc::directory_not_empty) return GuestEnotempty; + if (condition == std::errc::value_too_large) return GuestEoverflow; + const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 38ea7bb25..66038cda5 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -72,6 +72,9 @@ int main() { Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); +#ifndef _WIN32 + Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); +#endif std::error_code error; const auto link = (root / "link").string(); std::filesystem::create_symlink("sub/target.txt", link, error); @@ -85,6 +88,11 @@ int main() { Require(readlink_nid_postfix(link.c_str(), nullptr, 0) == 0); Require(readlink_nid_postfix(link.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); Require(readlink_nid_postfix((root / "LINK").string().c_str(), buffer, sizeof(buffer)) == 14); +#ifndef _WIN32 + std::filesystem::create_symlink("loopB", root / "loopA"); + std::filesystem::create_symlink("loopA", root / "loopB"); + Require(readlink_nid_postfix((root / "loopA" / "inner").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 62); +#endif } else { std::fprintf(stderr, "symbolic links unavailable: the link cases not tested\n"); } From ad14190c132ed6c28321b1f75424c894436c8da7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 10 Oct 2026 15:24:24 -0700 Subject: [PATCH 223/277] fix(libc): resolve ".." in guest paths against the directory before it Guest paths were normalized lexically before they reached the host, so "missing/../file" and "file/../x" dropped the component before "..": stat and open succeeded on the file, unlink deleted it and rmdir removed a directory, where FreeBSD's lookup() resolves ".." with VOP_LOOKUP in the directory reached so far and fails with ENOENT for a missing component and ENOTDIR for a file. POSIX path resolution (XBD 4.13) says the same. ".." is still collapsed lexically when the path before it is an existing directory, so case-insensitive matching and aliases behave as before. When it is not, the rest of the path is passed to the host unchanged and the host resolves it. Linux then reports ENOENT or ENOTDIR as FreeBSD does; Windows collapses ".." itself, so its results are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XYhZu5HokHMo9mfi8QU83m --- core/libs/prx/libc/src/General.cpp | 31 +++++++++++++++++++++++++---- core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 370ff3f13..9ac3ec004 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -115,6 +115,18 @@ struct WorkingDirectory { std::filesystem::path current = root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { + if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; + return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); +} +bool HostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { + try { + std::error_code error; + return std::filesystem::is_directory(HostPath(state, guest), error); + } catch (const std::filesystem::filesystem_error&) { + return false; + } +} std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; @@ -123,10 +135,21 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { // Preserve the existing ability to pass explicit native drive paths. if (input.has_root_name()) return input; #endif - auto guest = (std::filesystem::path("/") / state.current.lexically_relative(state.root)); - guest = (input.is_absolute() ? input : guest / input).lexically_normal(); - if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; - return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); + const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); + const auto relative = (input.is_absolute() ? input : current / input).relative_path(); + std::filesystem::path guest("/"); + for (auto part = relative.begin(); part != relative.end(); ++part) { + if (*part == "..") { + const auto parent = guest.lexically_normal(); + if (parent.has_relative_path() && !HostDirectory(state, parent)) { + auto host = HostPath(state, parent); + for (; part != relative.end(); ++part) host /= *part; + return host.make_preferred(); + } + } + guest /= *part; + } + return HostPath(state, guest.lexically_normal()); } #ifdef _WIN32 using WriteMark = FILETIME; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 23b5ac7fd..e808b0acd 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -294,6 +294,8 @@ int main() { Require(stat_nid_postfix("\xff\xfe", &status) == -1 && *__error_nid_postfix() == 2); FileStat dirStatus{}; Require(stat_nid_postfix((rootName + "/").c_str(), &dirStatus) == 0 && (dirStatus.st_mode & 0170000) == 0040000); + const auto viaParent = rootName + "/../" + root.filename().string() + "/present.txt"; + Require(stat_nid_postfix(viaParent.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(nullptr, &status) == -1 && *__error_nid_postfix() == 14); Require(stat_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); FileStat linkStatus{}; @@ -315,6 +317,12 @@ int main() { Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); Require(stat_nid_postfix(dangling.string().c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); + const auto viaMissing = rootName + "/missing/../present.txt"; + Require(stat_nid_postfix(viaMissing.c_str(), &status) == -1 && *__error_nid_postfix() == 2); + Require(open_nid_postfix(viaMissing.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 2); + Require(unlink_nid_postfix(viaMissing.c_str()) == -1 && *__error_nid_postfix() == 2 && std::filesystem::exists(present)); + Require(stat_nid_postfix((presentName + "/..").c_str(), &status) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelStat((presentName + "/../present.txt").c_str(), &status) == static_cast(0x80020014u)); Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; From 17827dd9ab09d978d6d0dfdb1f27f83709e939f6 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Sun, 11 Oct 2026 16:16:03 +0530 Subject: [PATCH 224/277] fix(filesystem): reject invalid parent traversal on Windows --- core/libs/prx/libSceAvPlayer/src/Source.cpp | 5 +- core/libs/prx/libSceFont/src/Open.cpp | 7 +- .../libs/prx/libc/include/FilesystemError.hpp | 11 +++ core/libs/prx/libc/src/Directory.cpp | 3 +- core/libs/prx/libc/src/FileAndHeap.cpp | 14 ++- core/libs/prx/libc/src/General.cpp | 29 +++--- core/libs/prx/libkernel/Apr/src/Apr.cpp | 4 +- core/libs/prx/libkernel/File/src/Open.cpp | 13 ++- core/libs/prx/libkernel/File/src/Stdio.cpp | 36 ++++--- core/libs/prx/libkernel_sys/Export.cpp | 9 +- core/libs/tests/GuestAmpr.cpp | 4 + core/libs/tests/GuestAvPlayer.cpp | 2 + core/libs/tests/GuestFilesystem.cpp | 94 +++++++++++++++++++ core/libs/tests/GuestFont.cpp | 3 + core/libs/tests/GuestKernelSys.cpp | 10 ++ docs/dev/TechnicalDebt.md | 2 +- 16 files changed, 207 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index d6baf7b3d..768358e69 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAvPlayer/include/AvPlayer.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include #include @@ -243,7 +244,9 @@ public: format->pb = replacement->Context(); format->flags |= AVFMT_FLAG_CUSTOM_IO; } else { - const auto resolved = ResolvePath_nid_no_patch(path.c_str()).u8string(); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(path.c_str(), hostPath) != 0) return false; + const auto resolved = hostPath.u8string(); url = "file:" + std::string(resolved.begin(), resolved.end()); } if (const int error = avformat_open_input(&format, replacement ? nullptr : url.c_str(), nullptr, nullptr); error < 0) { diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ae87bf31b..ef118bd83 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -15,6 +15,7 @@ #include #include "prx/libSceFont/include/FontInternal.hpp" +#include "prx/libc/include/FilesystemError.hpp" namespace { @@ -280,7 +281,11 @@ int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::ui return SCE_FONT_ERROR_INVALID_LIBRARY; } if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; - const std::filesystem::path hostPath = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; + std::filesystem::path hostPath; + if (path && ResolvePathError_nid_no_patch(path, hostPath) != 0) { + *pFontHandle = nullptr; + return SCE_FONT_ERROR_FS_OPEN_FAILED; + } const std::string hostPathString = hostPath.string(); std::uint32_t libraryLock = 0; const int beginRc = BeginOpen(library, pFontHandle, libraryLock); diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index e31bedd2a..4bec836c8 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESYSTEMERROR_HPP #include +#include "General.hpp" inline int FilesystemError(const std::error_code& error) { if (error == std::errc::no_such_file_or_directory) return 2; @@ -22,4 +23,14 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved) { + try { + resolved = ResolvePath_nid_no_patch(path); + return 0; + } catch (const std::filesystem::filesystem_error& error) { + if (error.code() == std::errc::illegal_byte_sequence) return 2; + return FilesystemError(error.code()); + } +} + #endif diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index c3eefced4..27c7332b1 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include #include #include @@ -79,7 +80,7 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { if (!*path) { errno = 2; return nullptr; } try { auto directory = std::make_unique(); - directory->path = ResolvePath_nid_no_patch(path); + if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } return directory.release(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..d6e255918 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -13,6 +13,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" struct GuestFileMode { @@ -83,7 +84,15 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } try { - const std::filesystem::path path = *filename ? ResolvePath_nid_no_patch(filename) : std::filesystem::path{}; + std::filesystem::path path; + if (*filename) { + if (const int error = ResolvePathError_nid_no_patch(filename, path)) { + stream->Close(); + if (stream->IsDynamic()) delete stream; + errno = error; + return nullptr; + } + } if (stream->Reopen(path, parsed->native.c_str())) { if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; @@ -103,7 +112,8 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) errno = EINVAL; return nullptr; } - const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); + 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) { diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 9ac3ec004..fefc33af4 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -119,13 +119,24 @@ std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::p if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); } -bool HostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { - try { - std::error_code error; - return std::filesystem::is_directory(HostPath(state, guest), error); - } catch (const std::filesystem::filesystem_error&) { - return false; +void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { + const auto host = HostPath(state, guest); + std::error_code error; + const auto status = std::filesystem::status(host, error); + if (std::filesystem::is_directory(status)) return; + if (!error) error = std::make_error_code(std::filesystem::exists(status) ? std::errc::not_a_directory : std::errc::no_such_file_or_directory); +#ifdef _WIN32 + if (error == std::errc::no_such_file_or_directory) { + for (auto prefix = host.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + std::error_code prefixError; + const auto prefixStatus = std::filesystem::status(prefix, prefixError); + if (!std::filesystem::exists(prefixStatus)) continue; + if (!std::filesystem::is_directory(prefixStatus)) error = std::make_error_code(std::errc::not_a_directory); + break; + } } +#endif + throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { std::string text(path); @@ -141,11 +152,7 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { for (auto part = relative.begin(); part != relative.end(); ++part) { if (*part == "..") { const auto parent = guest.lexically_normal(); - if (parent.has_relative_path() && !HostDirectory(state, parent)) { - auto host = HostPath(state, parent); - for (; part != relative.end(); ++part) host /= *part; - return host.make_preferred(); - } + if (parent.has_relative_path()) RequireHostDirectory(state, parent); } guest /= *part; } diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8aa37a332..7df214ab0 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" @@ -107,7 +108,8 @@ int _fail(int guestErrno) { bool _resolve(const char* guestPath, std::uint32_t* id, std::uint64_t* size) { if (!guestPath) return false; - auto hostPath = ResolvePath_nid_no_patch(guestPath); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(guestPath, hostPath) != 0) return false; std::uint64_t bytes = 0; if (!_fileSize(hostPath, bytes)) return false; std::lock_guard lock(g_filesLock); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ca7cbe5f1..c11dc6d81 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" @@ -142,7 +143,8 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { const int fd = File::OpenRandomDevice(); return fd < 0 ? SceErrorFromErrno(errno) : fd; } - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -229,11 +231,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; - try { - native = ResolvePath_nid_no_patch(path); - } catch (const std::filesystem::filesystem_error&) { - return SceErrorFromErrno(2); - } + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; const auto status = std::filesystem::status(native, error); @@ -252,7 +250,8 @@ int APS5_VABI sceKernelUnlink(const char* path) { throw std::invalid_argument(std::string(__func__) + ": path is null"); } if (*path == '\0') return SceErrorFromErrno(2); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..d383ac3c8 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" @@ -436,7 +437,8 @@ extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { if (const int error = PathError(path)) return PosixFailure(error); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno)); return 0; } @@ -575,7 +577,8 @@ int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { if (const int error = PathError(path)) return PosixFailure(error); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { const int nativeError = errno; const bool blockedByFile = (nativeError == ENOENT || nativeError == EINVAL) && HasNonDirectoryPrefix(native); @@ -685,7 +688,9 @@ int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); if (const int error = PathError(path)) return PosixFailure(error); - if (!File::FillLinkStat(ResolvePath_nid_no_patch(path), sb)) return PosixResult(SceErrorFromErrno(errno)); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); return 0; } @@ -698,8 +703,10 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; - return std::filesystem::exists(ResolvePath_nid_no_patch(path), error) ? 0 : SCE_KERNEL_ERROR_ENOENT; + return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { @@ -808,7 +815,8 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; if (path == nullptr) throw std::invalid_argument("sceKernelMkdir: path is null"); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code error; if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); @@ -1001,10 +1009,12 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int APS5_VABI sceKernelRename(const char* from, const char* to) { if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); if (*from == '\0' || *to == '\0') return SceErrorFromErrno(GUEST_ENOENT); - const auto source = ResolvePath_nid_no_patch(from); + std::filesystem::path source; + if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); std::error_code error; if (!std::filesystem::exists(source, error)) return SceErrorFromErrno(GUEST_ENOENT); - const auto destination = ResolvePath_nid_no_patch(to); + std::filesystem::path destination; + if (const int result = ResolvePathError_nid_no_patch(to, destination)) return SceKernelError(result); std::filesystem::rename(source, destination, error); if (error) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(source); @@ -1015,7 +1025,8 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { int APS5_VABI sceKernelRmdir(const char* path) { if (path == nullptr) throw std::invalid_argument("sceKernelRmdir: path is null"); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); @@ -1040,7 +1051,8 @@ extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeChmod(native, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1059,7 +1071,8 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt if (path == nullptr) throw std::invalid_argument("sceKernelTruncate: path is null"); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; const auto status = std::filesystem::status(native, error); if (!std::filesystem::exists(status)) return SceErrorFromErrno(GUEST_ENOENT); @@ -1082,7 +1095,8 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SCE_KERNEL_ERROR_EINVAL; } } - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); return 0; } diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index f477e9aa2..df4d2e486 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -15,6 +15,7 @@ #endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -75,7 +76,8 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { @@ -99,8 +101,9 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); - const auto source = ResolvePath_nid_no_patch(existing); - const auto destination = ResolvePath_nid_no_patch(created); + std::filesystem::path source, destination; + 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); if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 43b223fac..b0d349ad2 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -576,6 +576,10 @@ void TestResolveFailures() { Require(sceKernelAprResolveFilepathsToIdsAndFileSizesForEach(paths, 2, ids, sizes, results) == 1); Require(results[0] == 0 && results[1] == static_cast(0x80020002)); Require(ids[1] == 0xffffffffu && sizes[0] == 4 && sizes[1] == 0); + for (const char* invalid : {"ampr_resolve_missing.bin/../ampr_resolve_existing.bin", "ampr_resolve_existing.bin/../ampr_resolve_existing.bin"}) { + Require(sceKernelAprResolveFilepathsToIds(&invalid, 1, ids, &failed) == -1); + Require(*__error_nid_postfix() == 2 && failed == 0 && ids[0] == 0xffffffffu); + } std::remove(paths[0]); } diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index dad55fead..d6ca10995 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -723,6 +723,8 @@ void TestPlayback() { Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source"); Check(!sceAvPlayerIsActive(player), "active without a source"); Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted"); + Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4/../avplayer.mp4") == OperationFailed, "file parent accepted"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == OperationFailed, "second source accepted"); Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing"); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index e808b0acd..5474dee06 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libkernel/File/include/File.hpp" #include #include #include @@ -51,6 +54,13 @@ int APS5_VABI sceKernelUnlink(const char*); int APS5_VABI sceKernelRename(const char*, const char*); int APS5_VABI sceKernelRmdir(const char*); int APS5_VABI sceKernelMkdir(const char*, unsigned short); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelCheckReachability(const char*); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI chdir_nid_postfix(const char*); +void* APS5_VABI opendir_nid_postfix(const char*); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI pipe_nid_postfix(int*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); @@ -76,7 +86,91 @@ static void Check(bool value, int line) { } } #define Require(value) Check((value), __LINE__) + +static void TestParentTraversal() { + const auto root = std::filesystem::path("anyps5-parent-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "Existing")); + const auto present = (root / "present.txt").generic_string(); + { std::ofstream file(present); file << "preserved"; } + const auto directory = (root / "victim").generic_string(); + Require(std::filesystem::create_directory(directory)); + const auto checkInvalid = [&](const std::string& prefix, int error) { + const auto path = prefix + "/present.txt"; + const auto missingLeaf = prefix + "/new.txt"; + const int kernelError = static_cast(0x80020000u | static_cast(error)); + FileStat status{}; + Require(stat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(lstat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(sceKernelStat(path.c_str(), &status) == kernelError); + Require(open_nid_postfix(path.c_str(), 0, 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelOpen(path.c_str(), 0x601, 0600) == kernelError); + Require(sceKernelOpen(missingLeaf.c_str(), 0x601, 0600) == kernelError); + Require(access_nid_postfix(path.c_str(), 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelCheckReachability(path.c_str()) == kernelError); + Require(chmod_nid_postfix(path.c_str(), 0400) == -1 && *__error_nid_postfix() == error); + Require(sceKernelChmod_nid_postfix(path.c_str(), 0400) == kernelError); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 0) == kernelError); + Require(sceKernelUtimes_nid_postfix(path.c_str(), nullptr) == kernelError); + Require(unlink_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelUnlink(path.c_str()) == kernelError); + Require(remove_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(path.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(present.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRename(path.c_str(), missingLeaf.c_str()) == kernelError); + Require(sceKernelRename(present.c_str(), missingLeaf.c_str()) == kernelError); + const auto victim = prefix + "/victim"; + Require(rmdir_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRmdir(victim.c_str()) == kernelError); + Require(remove_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(mkdir_nid_postfix(missingLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == error); + Require(sceKernelMkdir(missingLeaf.c_str(), 0700) == kernelError); + Require(chdir_nid_postfix(prefix.c_str()) == -1 && *__error_nid_postfix() == error); + Require(opendir_nid_postfix(prefix.c_str()) == nullptr && *__error_nid_postfix() == error); + Require(fopen_nid_postfix(path.c_str(), "wb") == nullptr && *__error_nid_postfix() == error); + auto* stream = fopen_nid_postfix(present.c_str(), "rb"); + Require(stream != nullptr); + const int descriptor = stream->GuestState().descriptor; + Require(freopen_nid_postfix(path.c_str(), "wb", stream) == nullptr && *__error_nid_postfix() == error); + Require(!DescriptorIsOpen_nid_no_patch(descriptor)); + std::ifstream file(present); + std::string contents; + std::getline(file, contents); + Require(contents == "preserved" && std::filesystem::is_directory(directory)); + Require(!std::filesystem::exists(root / "new.txt")); + }; + checkInvalid(root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/present.txt/..", 20); + checkInvalid(root.generic_string() + "/present.txt/child/../..", 20); + checkInvalid("/" + root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/missing/../../" + root.generic_string(), 2); + AddPathAlias_nid_no_patch("parent-test-mount", std::filesystem::absolute(root).string().c_str()); + checkInvalid("/PARENT-TEST-MOUNT/missing/..", 2); + checkInvalid("/parent-test-mount/present.txt/..", 20); + FileStat status{}; + Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); + RemovePathAlias_nid_no_patch("parent-test-mount"); + std::error_code linkError; + std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); + if (!linkError) { + Require(sceKernelStat((root.generic_string() + "/directory-link/../present.txt").c_str(), &status) == 0 && status.st_size == 9); + std::filesystem::create_symlink("present.txt", root / "file-link"); + std::filesystem::create_directory_symlink("missing", root / "dangling-link"); + checkInvalid(root.generic_string() + "/file-link/..", 20); + checkInvalid(root.generic_string() + "/dangling-link/..", 2); + } + Require(chdir_nid_postfix(root.generic_string().c_str()) == 0); + Require(sceKernelStat("missing/../present.txt", &status) == static_cast(0x80020002u)); + Require(sceKernelStat("present.txt/..", &status) == static_cast(0x80020014u)); + Require(sceKernelStat("EXISTING/../present.txt", &status) == 0 && status.st_size == 9); + const int created = sceKernelOpen("Existing/../new.txt", 0x601, 0600); + Require(created >= 0 && sceKernelClose(created) == 0); + Require(chdir_nid_postfix("/") == 0); + std::filesystem::remove_all(root); +} + int main() { + TestParentTraversal(); 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/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index f87745e78..fccc8be80 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -25,6 +25,7 @@ int APS5_VABI sceFontSupportSystemFonts(FontLibrary); int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontFile(FontLibrary, const char*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); @@ -202,6 +203,8 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_ERROR_ALREADY_SPECIFIED); + font = reinterpret_cast(&memory); + Require(sceFontOpenFontFile(library, "anyps5-missing-font-parent/../font.ttf", 1, nullptr, &font) == SCE_FONT_ERROR_FS_OPEN_FAILED && font == nullptr); Require(sceFontOpenFontMemory(library, nullptr, 0, nullptr, &font) == SCE_FONT_ERROR_INVALID_PARAMETER && font == nullptr); Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 38ea7bb25..a4e869020 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -67,6 +67,16 @@ int main() { Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); char buffer[64]; + for (const auto& component : {std::string("missing"), std::string("file.txt")}) { + const auto prefix = root.generic_string() + "/" + component + "/../"; + const auto invalid = prefix + "file.txt"; + const auto created = prefix + "new.txt"; + const int expected = component == "missing" ? 2 : 20; + Require(readlink_nid_postfix(invalid.c_str(), buffer, sizeof(buffer)) == -1 && Error() == expected); + Require(link_nid_postfix(invalid.c_str(), created.c_str()) == -1 && Error() == expected); + Require(link_nid_postfix(file.c_str(), created.c_str()) == -1 && Error() == expected); + Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..75f825b6c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -571,7 +571,7 @@ Throughout the project, every function at every stage either **does exactly what - [Running application ID](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. - [sceNpCheckNpAvailability](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests - [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. -- [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. +- [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. - [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; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does, and passes every other conversion to the host's `strftime`. On Windows the conversions the CRT lacks (`%P` and the `%-`, `%_` and `%0` padding flags) still produce nothing, and the padding flags are not supported on the five new conversions at all (`%-k`, `%_k` and `%0k` go to the host), an unknown conversion, a doubled `E` or `O` modifier (`%EE`) or a trailing lone `%` returns 0 where FreeBSD prints `%` and the character, and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%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. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. - [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 From 2ad916566f99afd2c48681c3bb20a27a212f3594 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Sun, 11 Oct 2026 17:49:52 +0530 Subject: [PATCH 225/277] fix(libkernel_sys): return the resolver's error from symlink symlink, added in #3491 while this branch was open, still resolved its new path with ResolvePath_nid_no_patch, which now throws for an invalid "..". Use ResolvePathError_nid_no_patch as link does, so "missing/../x" returns ENOENT and "file/../x" ENOTDIR. Reported by @Silverash-i8 on #3485. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 3 ++- core/libs/tests/GuestKernelSys.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index df4d2e486..367fefd37 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -137,7 +137,8 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { NotImplemented_nid_no_patch("symlink: absolute targets"); return -1; } - const auto native = ResolvePath_nid_no_patch(created); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(created, native)) return static_cast(Fail(error)); std::error_code error; #ifdef _WIN32 if (GetFileAttributesW(native.wstring().c_str()) != INVALID_FILE_ATTRIBUTES) return static_cast(Fail(GuestEexist)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a4e869020..251a9b214 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -75,6 +75,7 @@ int main() { Require(readlink_nid_postfix(invalid.c_str(), buffer, sizeof(buffer)) == -1 && Error() == expected); Require(link_nid_postfix(invalid.c_str(), created.c_str()) == -1 && Error() == expected); Require(link_nid_postfix(file.c_str(), created.c_str()) == -1 && Error() == expected); + Require(symlink_nid_postfix("file.txt", created.c_str()) == -1 && Error() == expected); Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); From ba9f903e3fc1affeb5331c1038e85e387feb48ab Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:55 +0200 Subject: [PATCH 226/277] feat(shader): implement s_subvector_loop_begin and s_subvector_loop_end --- .../tests/execution/SubvectorLoop.cpp | 209 ++++++++++++++++++ .../SpirvBackend/src/SpirvSpecialization.cpp | 8 +- .../src/ControlFlowInstructions.cpp | 35 ++- .../Translation/src/EmbeddedVertexFetch.cpp | 2 + docs/dev/TechnicalDebt.md | 1 + 5 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp new file mode 100644 index 000000000..4944237df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -0,0 +1,209 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Columns = 9; +constexpr std::uint32_t Ones = 0xffffffffu; +constexpr std::uint32_t Marker = 0x5a5a5a5au; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecResetCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1, + 0xbe26fff5, 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, + 0x80010a01, 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, + 0x80010e01, 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, + 0x80011201, 0xbf810000, +}; + +constexpr std::array Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"}; + +struct Group { + std::uint32_t first; + std::uint32_t last; + std::array values; +}; + +struct Case { + const char* name; + std::uint64_t mask; + std::vector groups; +}; + +const std::vector ExecKeptCases{ + {"all lanes", ~0ull, { + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}}, + {32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}}, + }}, + {"low half", 0xffffffffull, { + {0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}}, + {32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}}, + {3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}}, + {40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}}, + }}, +}; + +const std::vector ExecResetCases{ + {"all lanes", ~0ull, { + {0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}}, + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}}, + }}, + {"low half", 0xffffffffull, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}}, + {3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 1}}, + }}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t mask) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::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, {}}, + {64, 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(const char* variant, const Case& testCase) { + std::array, Threads> expected{}; + for (const auto& group : testCase.groups) { + for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values; + } + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto actual = Output[column * Threads + lane]; + Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column])); + } + } +} + +void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 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.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what()); + return; + } + Require(false, "a wave32 subvector loop was recompiled"); +} + +void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span code, const std::vector& cases) { + for (const auto& testCase : cases) { + Run(device, code, testCase.mask); + Check(variant, testCase); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckWave32Throws(*device, ExecKeptCode); + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases); + RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases); + std::puts("subvector loop tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 188cae8f5..6d8d41477 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -417,6 +417,12 @@ private: } return true; }; + std::set loopHeaders; + for (const auto& [label, indices] : blocks) { + for (const auto index : indices) { + if (Opcode(instructions[index]) == spv::OpLoopMerge) loopHeaders.insert(label); + } + } std::map> edges; for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; @@ -425,7 +431,7 @@ private: bool loop = false; for (const auto index : indices) loop |= Opcode(instructions[index]) == spv::OpLoopMerge; if (!loop && op == spv::OpBranchConditional) { - if (const auto condition = value(terminal.at(1))) target = terminal.at(*condition != 0u ? 2u : 3u); + if (const auto condition = value(terminal.at(1)); condition.has_value() && !loopHeaders.contains(terminal.at(*condition != 0u ? 3u : 2u))) target = terminal.at(*condition != 0u ? 2u : 3u); } else if (!loop && op == spv::OpSwitch) { if (const auto selector = value(terminal.at(1))) { target = terminal.at(2); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 838346dc4..3fca25ebf 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -13,7 +13,40 @@ void TranslateControlFlowInstruction(IrBuilder& builder, const RdnaInstruction& } void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) { - throw std::runtime_error("GNM subvector loop instructions are not supported by this translator"); + if (program.WaveSize() != 64u) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " in a wave" + std::to_string(program.WaveSize()) + " shader is not measured, only wave64 subvector loops are supported"); + } + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " with a destination that is not an SGPR is not measured"); + } + const IrU32 execLo(ir.GetExecLo()); + const IrU32 execHi(ir.GetExecHi()); + const IrU32 counter = readRawU32(inst.destination); + const IrU32 zero(ir.Constant(0u)); + IrU32 newExecLo = execLo; + IrU32 newExecHi = execHi; + IrU32 newCounter = counter; + if (begin) { + const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); + const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); + const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); + newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); + newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); + instructionBranchCondition = execZero; + } else { + const IrU1 highSet(ir.INotEqual(execHi.Value(), zero.Value())); + const IrU1 counterZero(ir.IEqual(counter.Value(), zero.Value())); + const IrU1 stay(ir.LogicalOr(highSet.Value(), counterZero.Value())); + newExecLo = IrU32(ir.Select(highSet.Value(), counter.Value(), ir.Select(counterZero.Value(), execLo.Value(), zero.Value()))); + newExecHi = IrU32(ir.Select(stay.Value(), execHi.Value(), counter.Value())); + newCounter = IrU32(ir.Select(stay.Value(), counter.Value(), execLo.Value())); + instructionBranchCondition = IrU1(ir.LogicalNot(stay.Value())); + } + writeRawU32(inst.destination, newCounter); + ir.SetExecLo(newExecLo.Value()); + ir.SetExecHi(newExecHi.Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 7bc692922..1e2e2d5fc 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -342,6 +342,8 @@ bool writesExec(const RdnaInstruction& inst) { case RdnaOpcode::SOrn1SaveexecB64: case RdnaOpcode::SAndn1WrexecB64: case RdnaOpcode::SAndn2WrexecB64: + case RdnaOpcode::SSubvectorLoopBegin: + case RdnaOpcode::SSubvectorLoopEnd: return true; default: return false; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index af04129b1..ac9abc9a3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -105,6 +105,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge and the NVIDIA driver 615.78.08 crashes compiling such a loop; a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [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. From bf8a6bc2ff9741bf8c7c10951b7e83680cb8655a Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:35:01 -0400 Subject: [PATCH 227/277] test(libc): cover partial shared write collection --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 +++ .../libSceAgcDriver/tests/SharedWrites.cpp | 56 ++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6f112764f..348025bc8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1023,6 +1023,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) + set_tests_properties(agc_shared_write_partial_collect PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + add_test(NAME agc_shared_write_partial_collect_two_pass COMMAND agc_shared_write_tests --partial-collect) + set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp index 895cbc8de..1ba427592 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp @@ -118,6 +118,59 @@ void smallStores() { require(StoreOwnBytes(0, std::span{}) == 0, "empty store produced a version"); } +void partialCollect() { + constexpr std::size_t page = 4096; + Mapping mapping(page * 4); + auto* data = static_cast(mapping.data); +#ifdef _WIN32 + void* alias = nullptr; + require(sceKernelMapDirectMemory(&alias, mapping.size, 3, 0, mapping.physical, 65536) == 0, "partial collect alias failed"); +#endif + require(WriteWatched(), "write tracking unavailable for partial collect"); + const auto before = CollectWritesUncached(mapping.Address(), mapping.size); + require(before != 0, "partial collect range is not watched"); + data[page * 3] = std::byte{91}; + require(CollectWritesUncached(mapping.Address(), page) != 0, "prefix collect failed"); + require(!UnchangedSinceCollected(mapping.Address() + page * 3, page, before), "partial collect lost a neighboring guest write"); + const auto settled = CollectWritesUncached(mapping.Address(), mapping.size); + require(UnchangedSinceCollected(mapping.Address(), mapping.size, settled), "fully collected shared range remained dirty"); +#ifdef _WIN32 + const auto collect = [&](void* view, std::size_t offset, std::size_t bytes, std::size_t capacity, bool clear) { + std::array pages{}; + std::size_t count = capacity; + const auto address = reinterpret_cast(view); + require(GuestArena::GuestArenaCollectWrites_nid_postfix(address + offset, bytes, pages.data(), &count, clear), "shared subpage collection failed"); + std::vector offsets; + for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - address); + return offsets; + }; + const std::vector all{0, page, page * 2, page * 3}; + collect(alias, 0, mapping.size, 4, true); + data[page * 3] = std::byte{92}; + require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe missed a shared write"); + require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe consumed a shared write"); + require(collect(mapping.data, 0, page, 4, true) == std::vector{0}, "prefix reset missed a shared write"); + require(collect(alias, 0, mapping.size, 4, false) == all, "prefix reset consumed an alias write"); + require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "prefix reset consumed a neighboring write"); + require(collect(mapping.data, page * 3, page, 4, false).empty(), "reset subpage remained dirty"); + require(collect(mapping.data, page, page * 2, 4, true) == std::vector{page, page * 2}, "partial resets consumed intervening writes"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "collected subpages remained dirty"); + require(collect(alias, 0, mapping.size, 4, true) == all, "collecting one view consumed an alias write"); + static_cast(alias)[page] = std::byte{93}; + require(collect(mapping.data, page * 2, page, 4, true) == std::vector{page * 2}, "alias write did not invalidate the other view"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{0, page, page * 3}, "full collect after an interior reset did not preserve pending subpages"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "full collect left pending subpages"); + static_cast(alias)[page * 3] = std::byte{94}; + require(collect(mapping.data, 0, mapping.size, 1, true) == std::vector{0}, "limited collect returned the wrong prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false) == all, "limited collect consumed unreported writes"); + require(collect(mapping.data, page, page * 3, 3, true) == std::vector{page, page * 2, page * 3}, "limited collect continuation lost writes"); + require(collect(mapping.data, 0, page, 1, true) == std::vector{0}, "limited collect consumed its unreset prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "limited collection did not settle"); + require(sceKernelMunmap(alias, mapping.size) == 0, "partial collect alias unmap failed"); +#endif + std::cout << "Partial shared write collection tests passed\n"; +} + template void fails(TAction&& fn) { bool failed = false; try { fn(); } catch (const std::runtime_error&) { failed = true; } @@ -208,7 +261,8 @@ void run() { int main(int argc, char** argv) { try { - if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); + if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") partialCollect(); + else if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels") benchmark(true); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-fallback") benchmark(false, false); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels-fallback") benchmark(true, false); From e5e8c232e87f9817cd0a386a46d2b8f00b80c03a Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:35:18 -0400 Subject: [PATCH 228/277] fix(libc): preserve uncollected shared memory writes on Windows --- core/libs/prx/libc/include/WindowsMappings.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index b518d9cb8..9d33b9b0b 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -235,7 +235,7 @@ public: const auto found = views.find(base); if (found != views.end()) { auto& view = found->second; - const auto stop = std::min(end, base + pageBytes); + const auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); if (view.protection == PAGE_NOACCESS) return false; const bool pinned = view.page->pins != 0; if (pinned || view.seen != view.page->generation) { @@ -255,8 +255,8 @@ public: if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); other.armed = true; } - view.seen = view.page->generation; - rememberClean(base, base + pageBytes); + if (cursor == base && stop == base + pageBytes) view.seen = view.page->generation; + rememberClean(cursor, stop); } cursor = stop; } else { From 3b32e54ed90777c6dcdf6b7b5aa21f6cfb442bba Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:37:58 -0400 Subject: [PATCH 229/277] test(agc): skip partial collection when write watching is unavailable --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++-- core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 348025bc8..fc5d0b5c2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1024,10 +1024,10 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) - set_tests_properties(agc_shared_write_partial_collect PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) add_test(NAME agc_shared_write_partial_collect_two_pass COMMAND agc_shared_write_tests --partial-collect) - set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp index 1ba427592..cd11b3797 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp @@ -261,7 +261,13 @@ void run() { int main(int argc, char** argv) { try { - if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") partialCollect(); + if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") { + if (!WriteWatched()) { + std::cout << "Partial shared write collection skipped: write tracking unavailable\n"; + return 77; + } + partialCollect(); + } else if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels") benchmark(true); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-fallback") benchmark(false, false); From 8fbd0eecc10937aef434281ee555d6139cc4b5e8 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:44:58 -0400 Subject: [PATCH 230/277] feat(agc): capture and replay compute pipeline compilation --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 45 +++ .../include/ComputePipelineCapture.hpp | 76 +++++ .../Execution/include/VulkanDevice.hpp | 5 + .../Execution/src/ComputePipelineCapture.cpp | 261 ++++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 233 +++++++++++++++- .../Graphics/include/ShaderResources.hpp | 2 + .../Graphics/src/ShaderResources.cpp | 10 + .../tests/ComputePipelineCapture.cpp | 125 +++++++++ .../tests/ComputePipelineCaptureRun.cmake | 68 +++++ .../tests/ComputePipelineReplay.cpp | 82 ++++++ .../tools/ComputePipelineFixture.hpp | 35 +++ .../tools/ComputePipelineReplay.cpp | 154 +++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + docs/dev/BUILD.md | 18 ++ 14 files changed, 1100 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake create mode 100644 core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6f112764f..0d624dd89 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -111,6 +111,7 @@ add_custom_command( set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp @@ -1250,6 +1251,50 @@ endif() set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_replay) +add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp) +target_include_directories(agc_compute_pipeline_replay PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc) +set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_compute_pipeline_replay) + +if(BUILD_TESTING) + add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp) + target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) + set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) +endif() + +if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) + add_test_executable(agc_compute_pipeline_replay_tests tests/ComputePipelineReplay.cpp) + target_include_directories(agc_compute_pipeline_replay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + ) + target_link_libraries(agc_compute_pipeline_replay_tests PRIVATE libSceAgcDriver libc) + set_target_properties(agc_compute_pipeline_replay_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_replay_tests) + add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests) + set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests) + add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND} + -DFIXTURE=$ + -DREPLAY=$ + -DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake) + set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device") + if(WIN32) + set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(texture_format_lookup_tests ${LIBS_INCLUDE_DIR}/tests/TextureFormatLookup.cpp) target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp new file mode 100644 index 000000000..c4371f2b8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp @@ -0,0 +1,76 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct ComputeDescriptorBinding { + std::uint32_t binding = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + std::uint32_t count = 1; + VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT; + VkDescriptorBindingFlags flags = 0; +}; + +struct ComputeDescriptorSetLayout { + VkDescriptorSetLayoutCreateFlags flags = 0; + std::vector bindings; +}; + +struct ComputeSpecialization { + std::uint32_t id = 0; + std::uint32_t value = 0; +}; + +struct ComputePipelineCapture { + static constexpr std::uint32_t Version = 1; + static constexpr std::size_t MaxFileBytes = 20u * 1024u * 1024u; + std::vector spirv; + std::vector sets; + std::vector pushConstants; + std::vector specialization; + std::uint32_t requiredSubgroupSize = 0; + VkPipelineCreateFlags flags = 0; + VkPipelineShaderStageCreateFlags stageFlags = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + + void Validate() const; + std::vector Encode() const; + static ComputePipelineCapture Decode(std::span bytes); + static ComputePipelineCapture Read(const std::filesystem::path& path); + std::filesystem::path Write(const std::filesystem::path& directory) const; + std::string Identity() const; +}; + +struct ComputePipelineReplayResult { + double moduleMilliseconds = 0; + double layoutMilliseconds = 0; + double pipelineMilliseconds = 0; + double cacheMilliseconds = 0; + double totalMilliseconds = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + std::size_t initialCacheBytes = 0; + std::vector cache; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 37a54bcdd..21e443572 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/Recipe.hpp" +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -31,6 +32,9 @@ struct RecordedDispatch; class VulkanDevice { public: explicit VulkanDevice(const PresentationWindow* window = nullptr); + struct CompileOnly {}; + explicit VulkanDevice(CompileOnly); + ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache = {}); ~VulkanDevice(); VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; @@ -296,6 +300,7 @@ public: void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); private: + VulkanDevice(const PresentationWindow* window, bool compileOnly); // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp new file mode 100644 index 000000000..73bccb146 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -0,0 +1,261 @@ +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +void RequireCapture(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string("compute pipeline capture: ") + message); +} + +std::string Hash(std::span bytes) { + std::uint64_t hash = 14695981039346656037ull; + for (const auto value : bytes) { + hash ^= std::to_integer(value); + hash *= 1099511628211ull; + } + std::ostringstream result; + result << std::hex << std::setfill('0') << std::setw(16) << hash; + return result.str(); +} + +class Encoder { +public: + void Word(std::uint32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast((value >> shift) & 0xffu)); + } + void String(const std::string& value) { + Word(static_cast(value.size())); + for (const auto character : value) bytes.push_back(static_cast(static_cast(character))); + } + std::vector bytes; +}; + +class Decoder { +public: + explicit Decoder(std::span bytes) : bytes(bytes) {} + std::uint32_t Word() { + RequireCapture(bytes.size() - cursor >= 4, "truncated integer"); + std::uint32_t value = 0; + for (unsigned shift = 0; shift < 32; shift += 8) value |= std::to_integer(bytes[cursor++]) << shift; + return value; + } + std::uint32_t Count(std::uint32_t maximum, std::size_t minimumBytes) { + const auto count = Word(); + RequireCapture(count <= maximum && count <= (bytes.size() - cursor) / minimumBytes, "count exceeds record bounds"); + return count; + } + std::string String() { + const auto count = Count(VK_MAX_PHYSICAL_DEVICE_NAME_SIZE - 1, 1); + std::string result; + for (std::uint32_t index = 0; index < count; ++index) result.push_back(static_cast(std::to_integer(bytes[cursor++]))); + return result; + } + bool Finished() const { return cursor == bytes.size(); } +private: + std::span bytes; + std::size_t cursor = 0; +}; + +std::vector ReadBytes(const std::filesystem::path& path) { + const auto size = std::filesystem::file_size(path); + RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 20 MiB"); + std::ifstream input(path, std::ios::binary); + RequireCapture(input.good(), "cannot open capture"); + std::vector bytes(static_cast(size)); + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + RequireCapture(input.good() && input.peek() == std::char_traits::eof(), "capture changed while reading"); + return bytes; +} + +} + +void ComputePipelineCapture::Validate() const { + RequireCapture(spirv.size() >= 5 && spirv.size() <= 4u * 1024u * 1024u, "SPIR-V word count out of bounds"); + RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header"); + bool mainEntry = false; + for (std::size_t cursor = 5; cursor < spirv.size();) { + const auto count = spirv[cursor] >> 16u; + RequireCapture(count != 0 && count <= spirv.size() - cursor, "truncated SPIR-V instruction"); + if ((spirv[cursor] & 0xffffu) == 15u) { + RequireCapture(count >= 5 && spirv[cursor + 1] == 5u && spirv[cursor + 2] != 0 && spirv[cursor + 2] < spirv[3] && spirv[cursor + 3] == 0x6e69616du && spirv[cursor + 4] == 0, "only a compute main entry point is supported"); + RequireCapture(!mainEntry, "multiple entry points are unsupported"); + mainEntry = true; + } + cursor += count; + } + RequireCapture(mainEntry, "compute main entry point missing"); + RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name"); + RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags"); + RequireCapture(stageFlags == 0, "unsupported shader stage flags"); + RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size"); + RequireCapture(sets.size() <= 8, "too many descriptor sets"); + std::size_t totalBindings = 0; + for (const auto& layout : sets) { + RequireCapture((layout.flags & ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) == 0, "unsupported descriptor layout flags"); + RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings"); + totalBindings += layout.bindings.size(); + std::set used; + for (const auto& binding : layout.bindings) { + RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding"); + RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type"); + RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count"); + RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags"); + RequireCapture(binding.flags == 0 || (binding.flags == VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT && binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported descriptor binding flags"); + } + } + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + RequireCapture(pushConstants.size() <= 1, "too many compute push constant ranges"); + for (const auto& range : pushConstants) RequireCapture(range.stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && range.size != 0 && range.size <= 4096 && range.offset <= 4096 - range.size && (range.offset % 4) == 0 && (range.size % 4) == 0, "invalid push constant range"); + RequireCapture(specialization.size() <= 4096, "too many specialization values"); + std::set specializationIds; + for (const auto& value : specialization) RequireCapture(specializationIds.insert(value.id).second, "duplicate specialization ID"); +} + +std::vector ComputePipelineCapture::Encode() const { + Validate(); + Encoder output; + output.Word(0x35535041u); + output.Word(0x504d4f43u); + output.Word(Version); + output.Word(flags); + output.Word(stageFlags); + output.Word(requiredSubgroupSize); + output.Word(vendorId); + output.Word(deviceId); + output.Word(driverVersion); + output.Word(apiVersion); + for (const auto value : pipelineCacheUuid) output.Word(value); + output.String(deviceName); + output.Word(static_cast(sets.size())); + for (const auto& layout : sets) { + output.Word(layout.flags); + output.Word(static_cast(layout.bindings.size())); + for (const auto& binding : layout.bindings) { + output.Word(binding.binding); + output.Word(static_cast(binding.type)); + output.Word(binding.count); + output.Word(binding.stages); + output.Word(binding.flags); + } + } + output.Word(static_cast(pushConstants.size())); + for (const auto& range : pushConstants) { + output.Word(range.stageFlags); + output.Word(range.offset); + output.Word(range.size); + } + output.Word(static_cast(specialization.size())); + for (const auto& value : specialization) { + output.Word(value.id); + output.Word(value.value); + } + output.Word(static_cast(spirv.size())); + for (const auto word : spirv) output.Word(word); + RequireCapture(output.bytes.size() <= MaxFileBytes, "encoded capture exceeds size bound"); + return std::move(output.bytes); +} + +ComputePipelineCapture ComputePipelineCapture::Decode(std::span bytes) { + RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 20 MiB"); + Decoder input(bytes); + RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature"); + RequireCapture(input.Word() == Version, "unsupported file version"); + ComputePipelineCapture result; + result.flags = input.Word(); + result.stageFlags = input.Word(); + result.requiredSubgroupSize = input.Word(); + result.vendorId = input.Word(); + result.deviceId = input.Word(); + result.driverVersion = input.Word(); + result.apiVersion = input.Word(); + for (auto& value : result.pipelineCacheUuid) { + const auto word = input.Word(); + RequireCapture(word <= 255, "invalid cache UUID byte"); + value = static_cast(word); + } + result.deviceName = input.String(); + result.sets.resize(input.Count(8, 8)); + std::size_t totalBindings = 0; + for (auto& layout : result.sets) { + layout.flags = input.Word(); + const auto count = input.Count(4096, 20); + totalBindings += count; + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + layout.bindings.resize(count); + for (auto& binding : layout.bindings) { + binding.binding = input.Word(); + binding.type = static_cast(input.Word()); + binding.count = input.Word(); + binding.stages = input.Word(); + binding.flags = input.Word(); + } + } + result.pushConstants.resize(input.Count(1, 12)); + for (auto& range : result.pushConstants) { + range.stageFlags = input.Word(); + range.offset = input.Word(); + range.size = input.Word(); + } + result.specialization.resize(input.Count(4096, 8)); + for (auto& value : result.specialization) { + value.id = input.Word(); + value.value = input.Word(); + } + result.spirv.resize(input.Count(4u * 1024u * 1024u, 4)); + for (auto& word : result.spirv) word = input.Word(); + RequireCapture(input.Finished(), "trailing bytes after capture"); + result.Validate(); + return result; +} + +ComputePipelineCapture ComputePipelineCapture::Read(const std::filesystem::path& path) { + return Decode(ReadBytes(path)); +} + +std::string ComputePipelineCapture::Identity() const { + return Hash(Encode()); +} + +std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& directory) const { + const auto bytes = Encode(); + const auto recordDirectory = directory / ("compute-" + Hash(bytes)); + const auto path = recordDirectory / "capture.aps5cp"; + static std::mutex mutex; + std::lock_guard lock(mutex); + std::filesystem::create_directories(directory); + RequireCapture(!std::filesystem::is_symlink(std::filesystem::symlink_status(directory)), "capture directory is a symbolic link"); + if (std::filesystem::exists(recordDirectory)) { + RequireCapture(std::filesystem::is_directory(recordDirectory) && !std::filesystem::is_symlink(std::filesystem::symlink_status(recordDirectory)) && !std::filesystem::is_symlink(std::filesystem::symlink_status(path)), "existing capture path is not a regular capture"); + RequireCapture(ReadBytes(path) == bytes, "existing capture differs or is incomplete"); + return path; + } + std::uintmax_t totalBytes = 0; + std::size_t count = 0; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + RequireCapture(++count < 128, "capture directory reached 128 entries"); + RequireCapture(entry.is_directory() && !entry.is_symlink(), "capture directory contains an unexpected entry"); + const auto capture = entry.path() / "capture.aps5cp"; + RequireCapture(std::filesystem::is_regular_file(capture) && !std::filesystem::is_symlink(std::filesystem::symlink_status(capture)), "capture directory contains an incomplete record"); + const auto size = std::filesystem::file_size(capture); + RequireCapture(size <= MaxFileBytes, "existing capture exceeds size bound"); + totalBytes += size; + } + RequireCapture(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB"); + RequireCapture(std::filesystem::create_directory(recordDirectory), "capture directory was concurrently created"); + std::ofstream output(path, std::ios::binary); + RequireCapture(output.good(), "cannot create capture"); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + RequireCapture(output.good(), "cannot finish capture"); + return path; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87d211a49..5c7d732d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2,6 +2,9 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -70,6 +73,32 @@ void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason); } +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) { + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.flags = flags; + info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + info.stage.flags = stageFlags; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = module; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = specialization; + VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; + subgroup.requiredSubgroupSize = subgroupSize; + if (subgroupSize != 0) info.stage.pNext = &subgroup; + info.layout = layout; + check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); +} + +bool CaptureComputePipeline(std::size_t words) { + const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS"); + if (minimum == nullptr) return true; + std::uint32_t threshold = 0; + const auto end = minimum + std::strlen(minimum); + const auto parsed = std::from_chars(minimum, end, threshold); + require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= 4u * 1024u * 1024u, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 4194304"); + return words >= threshold; +} + // The ShaderResources content cache dispatches share with recorded draws: Graphics::ResourceCache, // one process-wide instance (see SharedResourceCache) that the State references so this file keeps // its Find/Insert/Remove/Clear calls; the device clears it at teardown before its descriptor caches @@ -178,6 +207,7 @@ struct WorkgroupMemory { }; struct VulkanDevice::State { + bool compileOnly = false; void* library = nullptr; PFN_vkGetInstanceProcAddr instanceProc = nullptr; PFN_vkGetDeviceProcAddr deviceProc = nullptr; @@ -567,6 +597,10 @@ struct VulkanDevice::State { ~State() { contextReady = false; + if (compileOnly && device != VK_NULL_HANDLE) { + reinterpret_cast(deviceProc(device, "vkDestroyDevice"))(device, nullptr); + device = VK_NULL_HANDLE; + } if (device != VK_NULL_HANDLE) { const auto idle = reinterpret_cast(deviceProc(device, "vkDeviceWaitIdle"))(device); if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); @@ -656,7 +690,159 @@ struct VulkanDevice::State { } }; -VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { +ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache) { + require(state->compileOnly, "pipeline replay requires a compile-only device"); + const auto start = std::chrono::steady_clock::now(); + const auto elapsed = [](auto since) { return std::chrono::duration(std::chrono::steady_clock::now() - since).count(); }; + capture.Validate(); + require(initialCache.size() <= 64u * 1024u * 1024u, "replay pipeline cache exceeds 64 MiB"); + if (!initialCache.empty()) { + require(initialCache.size() >= 32, "replay pipeline cache header is truncated"); + const auto word = [&](std::size_t offset) { + std::uint32_t result = 0; + for (unsigned shift = 0; shift < 32; shift += 8) result |= std::to_integer(initialCache[offset++]) << shift; + return result; + }; + require(word(0) >= 32 && word(0) <= initialCache.size() && word(4) == VK_PIPELINE_CACHE_HEADER_VERSION_ONE, "unsupported replay pipeline cache header"); + require(word(8) == state->properties.vendorID && word(12) == state->properties.deviceID, "replay pipeline cache belongs to another device"); + for (std::size_t index = 0; index < VK_UUID_SIZE; ++index) require(std::to_integer(initialCache[16 + index]) == state->properties.pipelineCacheUUID[index], "replay pipeline cache UUID differs from the driver"); + } + require((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) == 0 || state->pipelineExecutableInfo, "captured pipeline statistics feature is not enabled"); + if (capture.requiredSubgroupSize != 0) { + VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(state->physical, &properties); + require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported"); + } + const auto target = Target(); + require(capture.spirv[1] <= target.spirvVersion, "captured SPIR-V version exceeds enabled device support"); + for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) { + const auto count = capture.spirv[cursor] >> 16u; + if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) { + require(count == 2 && std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end(), "captured SPIR-V capability is not enabled"); + } + } +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(capture.spirv, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false)); +#else + throw std::runtime_error("compute pipeline replay requires ANYPS5_ENABLE_SPIRV_TOOLS=ON"); +#endif + ShaderRecompiler::RecompileResult shader; + shader.spirv = capture.spirv; + for (const auto& value : capture.specialization) shader.specialization.push_back({value.id, value.value}); + const Graphics::PipelineSpecialization specialization(shader); + const auto& limits = state->properties.limits; + require(capture.sets.size() <= limits.maxBoundDescriptorSets, "captured descriptor sets exceed device limits"); + for (const auto& range : capture.pushConstants) require(range.offset + range.size <= limits.maxPushConstantsSize, "captured push constants exceed device limits"); + struct ReplayObjects { + VkDevice device; + PFN_vkDestroyDescriptorSetLayout destroySetLayout; + PFN_vkDestroyPipelineCache destroyCache; + std::vector sets; + VkPipelineCache cache = VK_NULL_HANDLE; + ~ReplayObjects() { + for (auto layout : sets) destroySetLayout(device, layout, nullptr); + if (cache != VK_NULL_HANDLE) destroyCache(device, cache, nullptr); + } + } replay{state->device, state->DeviceFunction("vkDestroyDescriptorSetLayout"), state->DeviceFunction("vkDestroyPipelineCache"), {}}; + ComputePipelineObjects objects; + objects.device = state->device; + objects.destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects.destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects.destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + ComputePipelineReplayResult result; + result.deviceName = state->properties.deviceName; + result.vendorId = state->properties.vendorID; + result.deviceId = state->properties.deviceID; + result.driverVersion = state->properties.driverVersion; + result.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, result.pipelineCacheUuid.begin()); + result.initialCacheBytes = initialCache.size(); + auto phase = std::chrono::steady_clock::now(); + VkPipelineCacheCreateInfo cacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + cacheInfo.initialDataSize = initialCache.size(); + cacheInfo.pInitialData = initialCache.data(); + check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); + result.cacheMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = capture.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + result.moduleMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + std::array, 2> descriptorCounts{}; + bool hasUpdateAfterBindLayout = false; + for (const auto& layout : capture.sets) { + const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0; + hasUpdateAfterBindLayout |= updateAfterBind; + require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); + std::vector bindings; + std::vector flags; + for (const auto& binding : layout.bindings) { + bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr}); + flags.push_back(binding.flags); + const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3; + descriptorCounts[updateAfterBind ? 1 : 0][kind] += binding.count; + } + VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; + bindingFlags.bindingCount = static_cast(flags.size()); + bindingFlags.pBindingFlags = flags.data(); + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.flags = layout.flags; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT}; + state->DeviceFunction("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support); + require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported"); + VkDescriptorSetLayout handle = VK_NULL_HANDLE; + check(state->DeviceFunction("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay"); + replay.sets.push_back(handle); + } + const std::array regularLimits{std::min(limits.maxDescriptorSetStorageBuffers, limits.maxPerStageDescriptorStorageBuffers), std::min(limits.maxDescriptorSetSampledImages, limits.maxPerStageDescriptorSampledImages), std::min(limits.maxDescriptorSetStorageImages, limits.maxPerStageDescriptorStorageImages), std::min(limits.maxDescriptorSetSamplers, limits.maxPerStageDescriptorSamplers)}; + const auto& indexed = state->descriptorIndexingProperties; + const std::array indexedLimits{std::min(indexed.maxDescriptorSetUpdateAfterBindStorageBuffers, indexed.maxPerStageDescriptorUpdateAfterBindStorageBuffers), std::min(indexed.maxDescriptorSetUpdateAfterBindSampledImages, indexed.maxPerStageDescriptorUpdateAfterBindSampledImages), std::min(indexed.maxDescriptorSetUpdateAfterBindStorageImages, indexed.maxPerStageDescriptorUpdateAfterBindStorageImages), std::min(indexed.maxDescriptorSetUpdateAfterBindSamplers, indexed.maxPerStageDescriptorUpdateAfterBindSamplers)}; + std::uint64_t regularResources = 0; + std::uint64_t indexedResources = 0; + for (std::size_t kind = 0; kind < regularLimits.size(); ++kind) { + require(descriptorCounts[0][kind] <= regularLimits[kind], "captured pipeline descriptors exceed device limits"); + require(!hasUpdateAfterBindLayout || descriptorCounts[0][kind] + descriptorCounts[1][kind] <= indexedLimits[kind], "captured update-after-bind pipeline descriptors exceed device limits"); + if (kind != 3) { + regularResources += descriptorCounts[0][kind]; + indexedResources += descriptorCounts[0][kind] + descriptorCounts[1][kind]; + } + } + require(regularResources <= limits.maxPerStageResources && (!hasUpdateAfterBindLayout || indexedResources <= indexed.maxPerStageUpdateAfterBindResources), "captured pipeline resources exceed device limits"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = static_cast(replay.sets.size()); + layoutInfo.pSetLayouts = replay.sets.data(); + layoutInfo.pushConstantRangeCount = static_cast(capture.pushConstants.size()); + layoutInfo.pPushConstantRanges = capture.pushConstants.data(); + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout replay"); + result.layoutMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline); + result.pipelineMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + const auto getCache = state->DeviceFunction("vkGetPipelineCacheData"); + std::size_t cacheBytes = 0; + check(getCache(state->device, replay.cache, &cacheBytes, nullptr), "vkGetPipelineCacheData size replay"); + require(cacheBytes <= 64u * 1024u * 1024u, "exported replay pipeline cache exceeds 64 MiB"); + result.cache.resize(cacheBytes); + check(getCache(state->device, replay.cache, &cacheBytes, result.cache.data()), "vkGetPipelineCacheData replay"); + result.cache.resize(cacheBytes); + result.cacheMilliseconds += elapsed(phase); + result.totalMilliseconds = elapsed(start); + return result; +} + +VulkanDevice::VulkanDevice(const PresentationWindow* window) : VulkanDevice(window, false) {} + +VulkanDevice::VulkanDevice(CompileOnly) : VulkanDevice(nullptr, true) {} + +VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { + state->compileOnly = compileOnly; APS5_LOG_OUT("VulkanDevice constructor window=%p", static_cast(window)); #ifdef _WIN32 state->library = SDL_LoadObject("vulkan-1.dll"); @@ -966,10 +1152,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment; - Graphics::SetHostImportWindow(8ull << 20u); + if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. - if (const char* value = std::getenv("APS5_WORKING_SET_MIB")) { + if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { const SIZE_T bytes = static_cast(std::strtoull(value, nullptr, 10)) << 20u; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), bytes, bytes * 2, QUOTA_LIMITS_HARDWS_MIN_DISABLE | QUOTA_LIMITS_HARDWS_MAX_DISABLE)) std::fprintf(stderr, "[gpu] SetProcessWorkingSetSizeEx failed: %lu\n", GetLastError()); } @@ -1310,6 +1496,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u auto shaderProfile = std::make_unique(buildTarget(), deviceInfo, state->properties.limits); check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->shaderProfile = std::move(shaderProfile); + if (compileOnly) return; state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); 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}; @@ -3833,20 +4020,36 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - pipelineInfo.flags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - pipelineInfo.stage.module = objects->module; - pipelineInfo.stage.pName = "main"; + const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; const Graphics::PipelineSpecialization specialization(shader); - pipelineInfo.stage.pSpecializationInfo = specialization.Info(); - VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; - requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize); - if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup; - pipelineInfo.layout = objects->layout; + 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; + ComputeDescriptorSetLayout resourcesLayout; + resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + for (const auto& binding : resources->LayoutBindings()) { + require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); + const VkDescriptorBindingFlags flags = resources->LayoutUpdateAfterBind() && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0; + resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, flags}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + 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)); - check(state->DeviceFunction("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines"); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline); 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/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index dbfdf1581..2de705f4d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -114,6 +114,8 @@ public: ShaderResources(const ShaderResources&) = delete; ShaderResources& operator=(const ShaderResources&) = delete; VkDescriptorSetLayout Layout() const; + std::vector LayoutBindings() const; + bool LayoutUpdateAfterBind() const; void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..dd7d12639 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3322,6 +3322,16 @@ VkDescriptorSetLayout ShaderResources::Layout() const { return _layout; } +std::vector ShaderResources::LayoutBindings() const { + std::vector result; + for (const auto& binding : bindings) result.push_back(binding.layout); + return result; +} + +bool ShaderResources::LayoutUpdateAfterBind() const { + return updateAfterBind; +} + ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); } diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp new file mode 100644 index 000000000..7d7a85711 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -0,0 +1,125 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void Reject(TAction action, const char* message) { + bool rejected = false; + try { + action(); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, message); +} + +class Directory { +public: + Directory() { + path = std::filesystem::temp_directory_path() / ("anyps5-compute-capture-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(path), "could not create test directory"); + } + ~Directory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + std::filesystem::path path; +}; + +void CheckRoundTrip() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.deviceName = "test GPU \"name\""; + capture.vendorId = 0x10de; + capture.deviceId = 0x2b85; + capture.driverVersion = 0x12345678; + capture.apiVersion = VK_API_VERSION_1_3; + capture.pipelineCacheUuid[0] = 0x12; + capture.pipelineCacheUuid[15] = 0xab; + capture.requiredSubgroupSize = 32; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + capture.sets = {{VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, + {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}, + {7, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 32}}; + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + Require(decoded.Encode() == encoded, "capture round trip changed pipeline inputs"); + Require(decoded.Identity() == capture.Identity(), "capture identity changed after decoding"); + auto changed = decoded; + ++changed.specialization[0].value; + Require(changed.Identity() != capture.Identity(), "specialization did not affect capture identity"); + changed = decoded; + ++changed.sets[0].bindings[0].count; + Require(changed.Identity() != capture.Identity(), "descriptor layout did not affect capture identity"); + changed = decoded; + changed.requiredSubgroupSize = 64; + Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity"); + Directory directory; + const auto file = capture.Write(directory.path); + Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs"); + Require(capture.Write(directory.path) == file, "identical capture was not deduplicated"); + std::ofstream(file, std::ios::binary | std::ios::trunc).put('x'); + Reject([&] { capture.Write(directory.path); }, "existing damaged capture was overwritten"); + Require(std::filesystem::file_size(file) == 1, "damaged capture was changed"); +} + +void CheckMalformedRecords() { + const auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + for (std::size_t length = 0; length < encoded.size(); ++length) { + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(std::span(encoded).first(length)); }, "truncated capture accepted"); + } + auto bytes = encoded; + bytes.push_back(std::byte{0}); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "trailing capture bytes accepted"); + bytes = encoded; + bytes[0] ^= std::byte{1}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "invalid signature accepted"); + bytes = encoded; + bytes[8] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unknown capture version accepted"); + bytes = encoded; + for (std::size_t index = 104; index < 108; ++index) bytes[index] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unbounded device name accepted"); + const auto invalid = [&](auto change) { + auto copy = capture; + change(copy); + Reject([&] { copy.Encode(); }, "unsupported pipeline input accepted"); + }; + invalid([](auto& copy) { copy.spirv[0] = 0; }); + invalid([](auto& copy) { copy.spirv[5] = 0; }); + invalid([](auto& copy) { copy.spirv[11] = 4; }); + invalid([](auto& copy) { copy.spirv[13] = 0; }); + invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; }); + invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; }); + invalid([](auto& copy) { copy.requiredSubgroupSize = 3; }); + invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); }); + invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; }); + invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 1, 4}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{}, {}}}}; }); +} + +} + +int main() { + try { + CheckRoundTrip(); + CheckMalformedRecords(); + std::puts("compute pipeline capture: round trip, immutable writes, truncated records and unsupported inputs passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake new file mode 100644 index 000000000..fb1500757 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake @@ -0,0 +1,68 @@ +foreach(required IN ITEMS FIXTURE REPLAY OUTPUT_ROOT) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing ${required}") + endif() +endforeach() +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef runId) +set(runDirectory "${OUTPUT_ROOT}/compute-capture-${runId}") +set(captureDirectory "${runDirectory}/captures") +file(MAKE_DIRECTORY "${runDirectory}") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/normal.log" "${output}\n${error}") +if(result STREQUAL "77" AND NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN}) + message(STATUS "compute pipeline capture run: skipped, no usable Vulkan device") + file(REMOVE_RECURSE "${runDirectory}") + return() +endif() +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Normal compute fixture failed: ${result}\n${output}\n${error}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_REQUIRE_VULKAN=1" "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=${captureDirectory}" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/capture.log" "${output}\n${error}") +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Captured compute fixture failed: ${result}\n${output}\n${error}") +endif() +file(GLOB captures "${captureDirectory}/compute-*/capture.aps5cp") +list(LENGTH captures count) +if(count LESS 1 OR count GREATER 12) + message(FATAL_ERROR "Expected 1 to 12 compute captures, found ${count}") +endif() +foreach(capture IN LISTS captures) + get_filename_component(recordDirectory "${capture}" DIRECTORY) + get_filename_component(recordName "${recordDirectory}" NAME) + string(REPLACE "compute-" "" identity "${recordName}") + set(report "${runDirectory}/${recordName}.json") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "${REPLAY}" "${capture}" --output "${report}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 + ) + file(WRITE "${runDirectory}/${recordName}.log" "${output}\n${error}") + if(NOT result STREQUAL "0") + message(FATAL_ERROR "Captured pipeline replay failed: ${result}\n${output}\n${error}") + endif() + file(READ "${report}" reportJson) + string(JSON reportIdentity GET "${reportJson}" capture_identity) + string(JSON words GET "${reportJson}" spirv_words) + string(JSON sets GET "${reportJson}" descriptor_sets) + string(JSON ranges GET "${reportJson}" push_constant_ranges) + string(JSON dispatches GET "${reportJson}" dispatch_count) + string(JSON policy GET "${reportJson}" application_cache_policy) + string(JSON capturedDevice GET "${reportJson}" captured_device device_id) + string(JSON replayDevice GET "${reportJson}" replay_device device_id) + string(JSON capturedDriver GET "${reportJson}" captured_device driver_version_raw) + string(JSON replayDriver GET "${reportJson}" replay_device driver_version_raw) + if(NOT reportIdentity STREQUAL identity OR words LESS 54 OR sets LESS 1 OR ranges LESS 1 OR NOT dispatches EQUAL 0 OR NOT policy STREQUAL "fresh" OR NOT capturedDevice STREQUAL replayDevice OR NOT capturedDriver STREQUAL replayDriver) + message(FATAL_ERROR "Captured pipeline metadata mismatch: ${reportJson}") + endif() +endforeach() +message(STATUS "Normal and captured compute output verified; ${count} captured pipelines replayed without dispatch") +file(REMOVE_RECURSE "${runDirectory}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp new file mode 100644 index 000000000..076bfdd72 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp @@ -0,0 +1,82 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckResult(const AgcDriver::ComputePipelineReplayResult& result) { + Require(!result.deviceName.empty() && result.vendorId != 0 && result.apiVersion != 0, "replay did not report its device"); + Require(result.cache.size() >= 32, "replay did not export a pipeline cache header"); + for (const auto duration : {result.moduleMilliseconds, result.layoutMilliseconds, result.pipelineMilliseconds, result.cacheMilliseconds, result.totalMilliseconds}) { + Require(std::isfinite(duration) && duration >= 0, "replay reported an invalid duration"); + } + Require(result.totalMilliseconds >= result.pipelineMilliseconds, "pipeline duration exceeds replay duration"); +} + +} + +int main() { + try { + auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + std::unique_ptr device; + try { + device = std::make_unique(AgcDriver::VulkanDevice::CompileOnly{}); + } catch (const std::exception& error) { + const std::string_view message(error.what()); + const bool absent = (message.starts_with("Vulkan loader: ") && message != "Vulkan loader: vkGetInstanceProcAddr missing") || message == "Vulkan: no Vulkan 1.1 graphics and compute queue" || message == "vkCreateInstance: Vulkan result -9"; + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || !absent) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return 77; + } + const auto fresh = device->ReplayComputePipeline(decoded); + CheckResult(fresh); + Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes"); + const auto imported = device->ReplayComputePipeline(decoded, fresh.cache); + CheckResult(imported); + Require(imported.initialCacheBytes == fresh.cache.size(), "replay did not import the supplied cache"); + Require(fresh.deviceId == imported.deviceId && fresh.driverVersion == imported.driverVersion && fresh.pipelineCacheUuid == imported.pipelineCacheUuid, "replay device identity changed"); + Require(capture.Encode() == encoded, "replay modified the captured pipeline"); + auto corrupt = fresh.cache; + corrupt[8] ^= std::byte{0xff}; + bool rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache from another vendor was accepted"); + corrupt = fresh.cache; + corrupt[16] ^= std::byte{0xff}; + rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache with another pipeline UUID was accepted"); + capture.requiredSubgroupSize = 32; + try { + const auto subgroup = device->ReplayComputePipeline(capture); + CheckResult(subgroup); + std::puts("compute pipeline replay: required subgroup size 32 compiled"); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("captured required subgroup size is unsupported") != std::string_view::npos, "required subgroup replay failed without an explicit support refusal"); + std::puts("compute pipeline replay: device explicitly refused required subgroup size 32"); + } + std::printf("compute pipeline replay: fresh %.6f ms, imported %.6f ms, GPU %s; no dispatch\n", fresh.pipelineMilliseconds, imported.pipelineMilliseconds, fresh.deviceName.c_str()); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline replay test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp new file mode 100644 index 000000000..ea59418ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp @@ -0,0 +1,35 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP + +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" + +namespace AgcDriver { + +inline ComputePipelineCapture ComputePipelineFixture() { + ComputePipelineCapture capture; + capture.spirv = { + 0x07230203u, 0x00010000u, 0u, 7u, 0u, + 0x00020011u, 1u, + 0x0003000eu, 0u, 1u, + 0x0005000fu, 5u, 3u, 0x6e69616du, 0u, + 0x00060010u, 3u, 17u, 1u, 1u, 1u, + 0x00040047u, 6u, 1u, 7u, + 0x00020013u, 1u, + 0x00030021u, 2u, 1u, + 0x00040015u, 5u, 32u, 0u, + 0x00040032u, 5u, 6u, 1u, + 0x00050036u, 1u, 3u, 0u, 2u, + 0x000200f8u, 4u, + 0x000100fdu, + 0x00010038u + }; + capture.specialization.push_back({7u, 3u}); + capture.sets = {{0, {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 16}}; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + return capture; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp new file mode 100644 index 000000000..5baabe910 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp @@ -0,0 +1,154 @@ +#include "ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Options { + std::filesystem::path source; + std::filesystem::path cacheIn; + std::filesystem::path cacheOut; + std::filesystem::path output; + std::filesystem::path fixtureDirectory; + bool fixture = false; +}; + +Options Parse(int argc, char** argv) { + Options result; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + const auto value = [&](std::filesystem::path& destination) { + if (!destination.empty() || ++index == argc || *argv[index] == '\0') throw std::runtime_error("missing or repeated option: " + std::string(argument)); + destination = argv[index]; + }; + if (argument == "--cache-in") value(result.cacheIn); + else if (argument == "--cache-out") value(result.cacheOut); + else if (argument == "--output") value(result.output); + else if (argument == "--write-fixture") value(result.fixtureDirectory); + else if (argument == "--fixture" && !result.fixture) result.fixture = true; + else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument)); + else result.source = argument; + } + const auto sources = static_cast(!result.source.empty()) + static_cast(result.fixture) + static_cast(!result.fixtureDirectory.empty()); + if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY"); + if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options"); + for (const auto& path : {result.cacheOut, result.output}) { + if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + } + if (!result.cacheOut.empty() && !result.output.empty() && std::filesystem::absolute(result.cacheOut).lexically_normal() == std::filesystem::absolute(result.output).lexically_normal()) throw std::runtime_error("cache and report must use different paths"); + return result; +} + +std::vector ReadCache(const std::filesystem::path& path) { + if (path.empty()) return {}; + const auto bytes = std::filesystem::file_size(path); + if (bytes == 0 || bytes > 64u * 1024u * 1024u) throw std::runtime_error("imported pipeline cache must contain 1 byte to 64 MiB"); + std::vector result(static_cast(bytes)); + std::ifstream input(path, std::ios::binary); + input.read(reinterpret_cast(result.data()), static_cast(result.size())); + if (!input || input.peek() != std::char_traits::eof()) throw std::runtime_error("could not read complete pipeline cache: " + path.string()); + return result; +} + +void WriteBytes(const std::filesystem::path& path, std::span bytes) { + if (std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + std::ofstream output(path, std::ios::binary); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + if (!output) throw std::runtime_error("could not write complete output: " + path.string()); +} + +std::string Quote(std::string_view text) { + std::ostringstream result; + result << '"'; + for (const unsigned char character : text) { + if (character == '\\' || character == '"') result << '\\' << character; + else if (character < 0x20u) result << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(character); + else result << character; + } + result << '"'; + return result.str(); +} + +std::string Uuid(std::span bytes) { + std::ostringstream result; + for (const auto byte : bytes) result << std::hex << std::setw(2) << std::setfill('0') << static_cast(byte); + return result.str(); +} + +template +void WriteDevice(std::ostream& output, const TDevice& device) { + output << "{\"name\":" << Quote(device.deviceName) + << ",\"vendor_id\":" << device.vendorId << ",\"device_id\":" << device.deviceId + << ",\"driver_version_raw\":" << device.driverVersion << ",\"api_version_raw\":" << device.apiVersion + << ",\"pipeline_cache_uuid\":" << Quote(Uuid(device.pipelineCacheUuid)) << '}'; +} + +std::string Report(const Options& options, const AgcDriver::ComputePipelineCapture& capture, const AgcDriver::ComputePipelineReplayResult& replay) { + std::ostringstream output; + output << std::setprecision(12) + << "{\n \"schema_version\":1,\n \"source\":" << Quote(options.fixture ? "synthetic-no-op-compute" : options.source.string()) + << ",\n \"capture_identity\":" << Quote(capture.Identity()) + << ",\n \"capture_identity_algorithm\":\"fnv1a64\"" + << ",\n \"spirv_words\":" << capture.spirv.size() + << ",\n \"descriptor_sets\":" << capture.sets.size() + << ",\n \"push_constant_ranges\":" << capture.pushConstants.size() + << ",\n \"specialization_constants\":" << capture.specialization.size() + << ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize + << ",\n \"pipeline_flags\":" << capture.flags + << ",\n \"stage_flags\":" << capture.stageFlags + << ",\n \"captured_device\":"; + WriteDevice(output, capture); + output << ",\n \"replay_device\":"; + WriteDevice(output, replay); + output << ",\n \"application_cache_policy\":" << Quote(options.cacheIn.empty() ? "fresh" : "imported") + << ",\n \"cache_import_path\":" << Quote(options.cacheIn.string()) + << ",\n \"cache_import_bytes\":" << replay.initialCacheBytes + << ",\n \"cache_export_path\":" << Quote(options.cacheOut.string()) + << ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size()) + << ",\n \"driver_cache_state\":\"uncontrolled\"" + << ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false" + << ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds + << ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds + << ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n"; + return output.str(); +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--help") { + std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n" + "agc_compute_pipeline_replay --write-fixture DIRECTORY\n" + "Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n"; + return 0; + } + try { + const auto options = Parse(argc, argv); + if (!options.fixtureDirectory.empty()) { + std::cout << AgcDriver::ComputePipelineFixture().Write(options.fixtureDirectory).string() << '\n'; + return 0; + } + const auto* statistics = std::getenv("APS5_PIPELINE_STATS"); + if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking"); + const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source); + if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled"); + const auto cache = ReadCache(options.cacheIn); + AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{}); + const auto replay = device.ReplayComputePipeline(capture, cache); + if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache); + const auto report = Report(options, capture, replay); + if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size()))); + std::cout << report; + return 0; + } catch (const std::exception& error) { + std::cerr << "compute pipeline replay: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 1c3e6730a..88aafc8fe 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -246,6 +246,7 @@ function(add_shader_recompiler target runtimeTarget) SrgbUnormFormat ThreadOwned UnresolvableSwappcTarget + ValidateAndOptimizeSpirv FindUserDataCalls UserDataCalleeLength LinkUserDataCalls diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 50e7023fe..48216e4f1 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -89,6 +89,24 @@ ctest --test-dir build/write-watch --output-on-failure With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them. +## Compute pipeline replay + +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 20 MiB per record. Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. + +Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `4194304` fail. The filter applies only when capture is enabled. + +```sh +cmake --build build --target agc_compute_pipeline_replay +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-out pipeline.cache --output fresh.json +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json +``` + +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's device features and compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. + +Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. + +`--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. Each test has a 30-second timeout. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold. + ## Pipeline statistics Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default. From decbac9bb425a31ca965acb2b6cd43edb3681804 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:51:53 -0400 Subject: [PATCH 231/277] fix(agc): link capture tests with the Windows exception runtime --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++++ docs/dev/BUILD.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0d624dd89..b8c8d7323 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1265,9 +1265,14 @@ configure_windows_unwind(agc_compute_pipeline_replay) if(BUILD_TESTING) add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp) target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc) set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_capture_tests) add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compute_pipeline_capture PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 48216e4f1..2424f6d35 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -101,7 +101,7 @@ build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp - build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json ``` -On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's device features and compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's device features and compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. From a4ae6d7e311d98e415322e701d1f8f24df9f2d6c Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:14:44 -0400 Subject: [PATCH 232/277] fix(agc): capture larger compute modules and binding flags --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../include/ComputePipelineCapture.hpp | 3 +- .../Execution/src/ComputePipelineCapture.cpp | 14 ++++-- .../Execution/src/VulkanDevice.cpp | 38 +++++++++++--- .../Graphics/include/ShaderResources.hpp | 4 +- .../Graphics/src/ShaderResources.cpp | 24 ++++++--- .../tests/ComputePipelineCapture.cpp | 49 ++++++++++++++++++- .../tests/ComputePipelineReplay.cpp | 23 +++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 40 +++++++++++++++ docs/dev/BUILD.md | 6 +-- 10 files changed, 179 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b8c8d7323..090250044 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1268,6 +1268,11 @@ if(BUILD_TESTING) target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc) set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_compute_pipeline_capture_tests) + add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags) + set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp index c4371f2b8..270c29c79 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp @@ -32,7 +32,8 @@ struct ComputeSpecialization { struct ComputePipelineCapture { static constexpr std::uint32_t Version = 1; - static constexpr std::size_t MaxFileBytes = 20u * 1024u * 1024u; + static constexpr std::uint32_t MaxSpirvWords = 8u * 1024u * 1024u; + static constexpr std::size_t MaxFileBytes = 36u * 1024u * 1024u; std::vector spirv; std::vector sets; std::vector pushConstants; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 73bccb146..6137a82dc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -66,7 +66,7 @@ private: std::vector ReadBytes(const std::filesystem::path& path) { const auto size = std::filesystem::file_size(path); - RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 20 MiB"); + RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 36 MiB"); std::ifstream input(path, std::ios::binary); RequireCapture(input.good(), "cannot open capture"); std::vector bytes(static_cast(size)); @@ -78,7 +78,7 @@ std::vector ReadBytes(const std::filesystem::path& path) { } void ComputePipelineCapture::Validate() const { - RequireCapture(spirv.size() >= 5 && spirv.size() <= 4u * 1024u * 1024u, "SPIR-V word count out of bounds"); + RequireCapture(spirv.size() >= 5 && spirv.size() <= MaxSpirvWords, "SPIR-V word count out of bounds"); RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header"); bool mainEntry = false; for (std::size_t cursor = 5; cursor < spirv.size();) { @@ -103,12 +103,16 @@ void ComputePipelineCapture::Validate() const { RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings"); totalBindings += layout.bindings.size(); std::set used; + const auto highest = layout.bindings.empty() ? 0u : std::ranges::max(layout.bindings, {}, &ComputeDescriptorBinding::binding).binding; for (const auto& binding : layout.bindings) { RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding"); RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type"); RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count"); RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags"); - RequireCapture(binding.flags == 0 || (binding.flags == VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT && binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported descriptor binding flags"); + constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding"); } } RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); @@ -164,7 +168,7 @@ std::vector ComputePipelineCapture::Encode() const { } ComputePipelineCapture ComputePipelineCapture::Decode(std::span bytes) { - RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 20 MiB"); + RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 36 MiB"); Decoder input(bytes); RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature"); RequireCapture(input.Word() == Version, "unsupported file version"); @@ -209,7 +213,7 @@ ComputePipelineCapture ComputePipelineCapture::Decode(std::span value.id = input.Word(); value.value = input.Word(); } - result.spirv.resize(input.Count(4u * 1024u * 1024u, 4)); + result.spirv.resize(input.Count(MaxSpirvWords, 4)); for (auto& word : result.spirv) word = input.Word(); RequireCapture(input.Finished(), "trailing bytes after capture"); result.Validate(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5c7d732d0..3c4750421 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -95,7 +95,7 @@ bool CaptureComputePipeline(std::size_t words) { std::uint32_t threshold = 0; const auto end = minimum + std::strlen(minimum); const auto parsed = std::from_chars(minimum, end, threshold); - require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= 4u * 1024u * 1024u, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 4194304"); + require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= ComputePipelineCapture::MaxSpirvWords, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 8388608"); return words >= threshold; } @@ -250,6 +250,9 @@ struct VulkanDevice::State { // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; bool storageBufferUpdateAfterBind = false; + bool descriptorBindingPartiallyBound = false; + bool descriptorBindingVariableDescriptorCount = false; + bool runtimeDescriptorArray = false; VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT}; bool imageInt64Atomics = false; bool bufferInt64Atomics = false; @@ -779,7 +782,11 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); std::vector bindings; std::vector flags; + std::uint32_t variableCount = 0; for (const auto& binding : layout.bindings) { + require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported"); + require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported"); + if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count; bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr}); flags.push_back(binding.flags); const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3; @@ -789,13 +796,15 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip bindingFlags.bindingCount = static_cast(flags.size()); bindingFlags.pBindingFlags = flags.data(); VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; info.flags = layout.flags; info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data(); - VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT}; + VkDescriptorSetVariableDescriptorCountLayoutSupport variableSupport{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_LAYOUT_SUPPORT}; + VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT, variableCount != 0 ? &variableSupport : nullptr}; state->DeviceFunction("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support); require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported"); + require(variableCount <= variableSupport.maxVariableDescriptorCount, "captured variable descriptor count exceeds layout support"); VkDescriptorSetLayout handle = VK_NULL_HANDLE; check(state->DeviceFunction("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay"); replay.sets.push_back(handle); @@ -1333,13 +1342,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE; + state->descriptorBindingPartiallyBound = compileOnly && descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE; + state->descriptorBindingVariableDescriptorCount = compileOnly && descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE; + state->runtimeDescriptorArray = compileOnly && descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE; if (state->storageBufferUpdateAfterBind) { VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); } } descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); if (state->descriptorIndexing) { descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; @@ -1349,6 +1361,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); } descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE; + if (state->runtimeDescriptorArray) { + state->capabilities.push_back(spv::CapabilityRuntimeDescriptorArray); + if (!state->descriptorIndexing) state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); + } state->samplerAnisotropy = true; state->textureCompressionBC = true; deviceInfo.pEnabledFeatures = &enabled; @@ -1416,7 +1435,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : imageRobustnessFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageRobustnessFeatures; } - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) { + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } @@ -4028,10 +4047,13 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec capture.spirv = shader.spirv; ComputeDescriptorSetLayout resourcesLayout; resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - for (const auto& binding : resources->LayoutBindings()) { + const auto layoutBindings = resources->LayoutBindings(); + const auto bindingFlags = resources->LayoutBindingFlags(); + require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); + for (std::size_t index = 0; index < layoutBindings.size(); ++index) { + const auto& binding = layoutBindings[index]; require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); - const VkDescriptorBindingFlags flags = resources->LayoutUpdateAfterBind() && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0; - resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, flags}); + resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); } capture.sets.push_back(std::move(resourcesLayout)); if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 2de705f4d..f151fb378 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -60,7 +60,7 @@ public: // The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds // it from `bindings`), created on first use. - VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false); + VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false, std::span flags = {}); struct SetAllocation { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE; @@ -116,6 +116,7 @@ public: VkDescriptorSetLayout Layout() const; std::vector LayoutBindings() const; bool LayoutUpdateAfterBind() const; + std::span LayoutBindingFlags() const { return layoutBindingFlags; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -475,6 +476,7 @@ private: // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; + std::vector layoutBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index dd7d12639..c0ed92210 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1135,14 +1135,13 @@ void ShaderResources::build(std::span shaders, const Color namespace { -VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind) { - std::vector flags; - for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); +VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind, std::span flags) { + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; bindingFlags.bindingCount = static_cast(flags.size()); bindingFlags.pBindingFlags = flags.data(); VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data(); @@ -1404,16 +1403,17 @@ void ShaderResources::buildPrepare(std::span shaders, cons std::vector description; for (const auto& binding : bindings) { description.push_back(binding.layout); + layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); } if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind); + _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags); } else { ValidateDescriptorLayout(context, description, updateAfterBind); - _layout = CreateSetLayout(context, description, updateAfterBind); + _layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags); ownsLayout = true; } if (!bindings.empty()) { @@ -2617,16 +2617,24 @@ DescriptorCache::~DescriptorCache() { for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr); } -VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind) { +VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind, std::span flags) { + std::vector defaults; + if (flags.empty()) { + for (const auto& binding : bindings) defaults.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + flags = defaults; + } + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); std::lock_guard lock(mutex); std::vector keyCopy(key.begin(), key.end()); + keyCopy.push_back(static_cast(flags.size())); + keyCopy.insert(keyCopy.end(), flags.begin(), flags.end()); if (const auto found = layouts.find(keyCopy); found != layouts.end()) { ++stats.layoutHits; return found->second; } ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; - const auto layout = CreateSetLayout(context, bindings, updateAfterBind); + const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags); layouts.emplace(std::move(keyCopy), layout); return layout; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp index 7d7a85711..5324f2e43 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -71,6 +71,51 @@ void CheckRoundTrip() { Require(std::filesystem::file_size(file) == 1, "damaged capture was changed"); } +void CheckLargeRecord() { + auto capture = AgcDriver::ComputePipelineFixture(); + constexpr std::size_t words = 5500000; + capture.spirv.insert(capture.spirv.end() - 2, words - capture.spirv.size(), 0x00010000u); + const auto encoded = capture.Encode(); + Require(encoded.size() > 20u * 1024u * 1024u, "large fixture did not exceed the former file bound"); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).spirv == capture.spirv, "large module changed during decoding"); + Directory directory; + const auto file = capture.Write(directory.path); + Require(AgcDriver::ComputePipelineCapture::Read(file).spirv == capture.spirv, "large module changed on disk"); + capture.spirv.resize(AgcDriver::ComputePipelineCapture::MaxSpirvWords + 1u, 0x00010000u); + Reject([&] { capture.Encode(); }, "module beyond the word bound accepted"); + std::vector oversized(AgcDriver::ComputePipelineCapture::MaxFileBytes + 1u); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(oversized); }, "record beyond the file bound accepted"); + const auto oversizedFile = directory.path / "oversized.aps5cp"; + { + std::ofstream output(oversizedFile, std::ios::binary); + output.seekp(static_cast(AgcDriver::ComputePipelineCapture::MaxFileBytes)); + output.put('x'); + Require(output.good(), "cannot write oversized capture fixture"); + } + Reject([&] { AgcDriver::ComputePipelineCapture::Read(oversizedFile); }, "file beyond the file bound accepted"); +} + +void CheckBindingFlags() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.sets = {{0, {{7, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT}, + {3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}}}}; + const auto encoded = capture.Encode(); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).Encode() == encoded, "binding flags changed without update-after-bind"); + auto changed = capture; + changed.sets[0].bindings[0].flags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + Require(changed.Identity() != capture.Identity(), "binding flags did not affect capture identity"); + changed = capture; + changed.sets[0].bindings[1].flags |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + Reject([&] { changed.Encode(); }, "non-highest variable descriptor binding accepted"); + changed = capture; + changed.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT; + Reject([&] { changed.Encode(); }, "unsupported binding flags accepted"); + capture.sets[0].flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + capture.sets[0].bindings[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + capture.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + Require(AgcDriver::ComputePipelineCapture::Decode(capture.Encode()).Encode() == capture.Encode(), "combined binding flags changed"); +} + void CheckMalformedRecords() { const auto capture = AgcDriver::ComputePipelineFixture(); const auto encoded = capture.Encode(); @@ -116,7 +161,9 @@ int main() { try { CheckRoundTrip(); CheckMalformedRecords(); - std::puts("compute pipeline capture: round trip, immutable writes, truncated records and unsupported inputs passed"); + CheckBindingFlags(); + CheckLargeRecord(); + std::puts("compute pipeline capture: round trip, 5500000-word module, binding flags, immutable writes and bounded inputs passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp index 076bfdd72..df6e3b8b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp @@ -64,6 +64,29 @@ int main() { rejected = true; } Require(rejected, "cache with another pipeline UUID was accepted"); + for (const auto flags : {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)}) { + auto indexed = decoded; + indexed.sets[0].bindings[0].flags = flags; + try { + CheckResult(device->ReplayComputePipeline(indexed)); + std::printf("compute pipeline replay: descriptor binding flags 0x%x compiled without update-after-bind\n", static_cast(flags)); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + const std::string_view message(error.what()); + Require(message == "Vulkan presentation: captured partially-bound descriptors are unsupported" || message == "Vulkan presentation: captured variable descriptor count is unsupported", "binding flag replay failed without an explicit feature refusal"); + std::printf("compute pipeline replay: descriptor binding flags 0x%x explicitly refused: %s\n", static_cast(flags), error.what()); + } + } + auto runtimeArray = decoded; + runtimeArray.spirv.insert(runtimeArray.spirv.begin() + 7, {0x00020011u, 5302u, 0x0008000au, 0x5f565053u, 0x5f545845u, 0x63736564u, 0x74706972u, 0x695f726fu, 0x7865646eu, 0x00676e69u}); + try { + CheckResult(device->ReplayComputePipeline(runtimeArray)); + std::puts("compute pipeline replay: runtime descriptor array capability compiled"); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "runtime descriptor array replay failed without an explicit feature refusal"); + std::puts("compute pipeline replay: device explicitly refused runtime descriptor array capability"); + } capture.requiredSubgroupSize = 32; try { const auto subgroup = device->ReplayComputePipeline(capture); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..bce95291a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2149,6 +2149,41 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); } +void descriptorLayoutFlagTests() { + constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + mock = MockVulkan{}; + { + AgcDriver::Graphics::DescriptorCache cache(mockContext()); + const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + const std::array key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT}; + const std::array flags{indexed}; + const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags); + Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector{indexed}, "native layout lost binding flags without update-after-bind"); + Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache"); + Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags"); + } + Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects"); + for (const bool updateAfterBind : {false, true}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + { + ShaderRecompiler::RecompileResult program; + const std::uint32_t count = updateAfterBind ? 17 : 1; + program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector(4 * count))); + const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + AgcDriver::Graphics::ShaderResources resources(context, compute); + const auto flags = resources.LayoutBindingFlags(); + Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout"); + Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout"); + Require(updateAfterBind ? mock.layoutBindingFlags == std::vector(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector"); + } + Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects"); + } +} + void descriptorPoolTests() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; @@ -4248,6 +4283,11 @@ int main(int argc, char** argv) { setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { + 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]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 2424f6d35..43c74cb7d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -91,9 +91,9 @@ With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Roset ## Compute pipeline replay -Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 20 MiB per record. Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. -Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `4194304` fail. The filter applies only when capture is enabled. +Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. ```sh cmake --build build --target agc_compute_pipeline_replay @@ -101,7 +101,7 @@ build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp - build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json ``` -On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's device features and compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. From 598e991bca21695ec316bce908d5fd00449acff8 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:20:28 -0400 Subject: [PATCH 233/277] test(agc): provide the binding flag fixture placeholder buffer --- 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 bce95291a..5a6b9a32f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2166,6 +2166,7 @@ void descriptorLayoutFlagTests() { for (const bool updateAfterBind : {false, true}) { mock = MockVulkan{}; auto context = mockContext(); + context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; From 25f5afe6456d26b6ca976c28c9c5c08ad30a999a Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Sun, 11 Oct 2026 17:25:29 -0300 Subject: [PATCH 234/277] fix(agc): keep another device's host imports until its teardown --- .../Graphics/src/GuestBufferMemory.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 6891b95eb..c53b44567 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,8 @@ bool addressSpaceCacheEnabled() { return !disabled; } +struct RetiredImport; + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -148,6 +151,7 @@ struct HostImports { PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; std::map imports; + std::map>> parked; std::uint64_t liveBytes = 0; std::set failed; std::set unaliasedSpans; @@ -165,11 +169,6 @@ struct HostImports { bool dmaBufImportsStampPages = true; }; -HostImports& Imports() { - static HostImports imports; - return imports; -} - void destroyImport(const Context& context, const HostImport& entry) { context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); @@ -198,6 +197,11 @@ struct RetiredImport { HostImport entry; }; +HostImports& Imports() { + static HostImports imports; + return imports; +} + // Drops an import. Recorded batches (dispatches, recorded draws, GPU label writes) may still read it, // so the objects live until the batch open now completed; with no batches in flight all GPU work that // used it was synchronous and it is destroyed at once. @@ -763,8 +767,8 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc if (state.device != context.device) { for (const auto& [address, entry] : state.imports) { if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { - state.destroyBuffer(state.device, entry.buffer, nullptr); - state.freeMemory(state.device, entry.memory, nullptr); + state.parked[state.device].push_back(std::make_unique(state.device, state.destroyBuffer, state.freeMemory, entry)); + continue; } #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); @@ -1731,6 +1735,7 @@ void ClearImageMirrors(VkDevice device) { void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); + state.parked.erase(device); if (state.device != device) return; for (const auto& [address, entry] : state.imports) { state.destroyBuffer(state.device, entry.buffer, nullptr); From 3740c233bca5f89ef744e7cfb7d7d02b0a82d848 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:28:38 -0400 Subject: [PATCH 235/277] fix(agc): accept core Vulkan capabilities during compute replay --- .../Execution/src/VulkanDevice.cpp | 20 ++++++++++++++++++- .../tests/ComputePipelineReplay.cpp | 17 ++++++++++++++++ docs/dev/BUILD.md | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3c4750421..b3fdb6860 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -73,6 +73,24 @@ void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason); } +bool CoreVulkanCapability(std::uint32_t capability) { + switch (capability) { + case spv::CapabilityMatrix: + case spv::CapabilityShader: + case spv::CapabilityInputAttachment: + case spv::CapabilitySampled1D: + case spv::CapabilityImage1D: + case spv::CapabilitySampledBuffer: + case spv::CapabilityImageBuffer: + case spv::CapabilityImageQuery: + case spv::CapabilityDerivativeControl: + case spv::CapabilityStorageImageExtendedFormats: + return true; + default: + return false; + } +} + 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) { VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; info.flags = flags; @@ -722,7 +740,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) { const auto count = capture.spirv[cursor] >> 16u; if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) { - require(count == 2 && std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end(), "captured SPIR-V capability is not enabled"); + require(count == 2 && (CoreVulkanCapability(capture.spirv[cursor + 1]) || std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end()), "captured SPIR-V capability is not enabled"); } } #if ANYPS5_ENABLE_SPIRV_TOOLS diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp index df6e3b8b5..3e1f17936 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp @@ -38,6 +38,23 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return 77; } + auto sampled1D = decoded; + sampled1D.spirv.insert(sampled1D.spirv.begin() + 7, {0x00020011u, 43u}); + CheckResult(device->ReplayComputePipeline(sampled1D)); + std::puts("compute pipeline replay: core Vulkan Sampled1D capability compiled"); + for (const auto capability : {6u, 0xffffffffu}) { + auto unsupported = decoded; + unsupported.spirv.insert(unsupported.spirv.begin() + 7, {0x00020011u, capability}); + bool refused = false; + try { + device->ReplayComputePipeline(unsupported); + } catch (const std::exception& error) { + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "unsupported capability failed outside the replay capability guard"); + refused = true; + } + Require(refused, "non-Vulkan or unknown SPIR-V capability accepted"); + } + std::puts("compute pipeline replay: non-Vulkan Kernel and unknown capabilities rejected"); const auto fresh = device->ReplayComputePipeline(decoded); CheckResult(fresh); Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes"); diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 43c74cb7d..9ad3ab0ed 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -101,7 +101,7 @@ build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp - build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json ``` -On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Replay accepts the unconditional Vulkan 1.0 capabilities listed in the pinned [Vulkan registry](https://github.com/KhronosGroup/Vulkan-Headers/blob/2fa203425eb4af9dfc6b03f97ef72b0b5bcb8350/registry/vk.xml); other capabilities must be enabled by the device target. Non-Vulkan capabilities such as Kernel and unknown capabilities fail before pipeline creation. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. From 4966917282304f199ee62994d76740ba808248d7 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Sun, 11 Oct 2026 17:38:22 -0300 Subject: [PATCH 236/277] fix(agc): park retired host imports through shared_ptr so Imports() stays in place --- .../Graphics/src/GuestBufferMemory.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c53b44567..73e1015e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -114,6 +113,8 @@ struct GuestBufferMemory::AddressSpace { namespace { +struct RetiredImport; + struct AddressSpaceCache { std::atomic> current; std::atomic serials{0}; @@ -141,8 +142,6 @@ bool addressSpaceCacheEnabled() { return !disabled; } -struct RetiredImport; - // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -151,7 +150,7 @@ struct HostImports { PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; std::map imports; - std::map>> parked; + std::map>> parked; std::uint64_t liveBytes = 0; std::set failed; std::set unaliasedSpans; @@ -169,6 +168,11 @@ struct HostImports { bool dmaBufImportsStampPages = true; }; +HostImports& Imports() { + static HostImports imports; + return imports; +} + void destroyImport(const Context& context, const HostImport& entry) { context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); @@ -197,11 +201,6 @@ struct RetiredImport { HostImport entry; }; -HostImports& Imports() { - static HostImports imports; - return imports; -} - // Drops an import. Recorded batches (dispatches, recorded draws, GPU label writes) may still read it, // so the objects live until the batch open now completed; with no batches in flight all GPU work that // used it was synchronous and it is destroyed at once. @@ -767,7 +766,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc if (state.device != context.device) { for (const auto& [address, entry] : state.imports) { if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { - state.parked[state.device].push_back(std::make_unique(state.device, state.destroyBuffer, state.freeMemory, entry)); + state.parked[state.device].push_back(std::make_shared(state.device, state.destroyBuffer, state.freeMemory, entry)); continue; } #ifdef _WIN32 From 0ad1a4688429d36d6911711ed1446d0d209db0b4 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sun, 11 Oct 2026 22:44:57 +0200 Subject: [PATCH 237/277] test(agc): reject a split SWAP_ALT draw whose vertex shader stores to memory The 8_8 SWAP_ALT blend test now also draws with a vertex shader that stores a dword to a buffer (buffer_store_dword through user data 4-7) and a separate alpha blend, and requires the draw to be rejected for its shaders writing memory. With the former pixel-only check the draw went through and the test fails. --- .../tests/execution/SwapAltBlend.cpp | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp index 432c19bb8..393d98a1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace { @@ -53,6 +54,12 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; +alignas(256) constexpr std::array StoringVertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xe0700000, 0x80010000, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) std::array Stored{}; + 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} }}; @@ -163,16 +170,19 @@ VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor de return blend; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span pixelCode) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span pixelCode, std::span vertexCode = VertexCode) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; - std::vector vertexUserData(4, 0u); + std::vector vertexUserData(8, 0u); const auto address = reinterpret_cast(Triangle.data()); const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + const auto stored = reinterpret_cast(Stored.data()); + const std::array storeBuffer{static_cast(stored), static_cast((stored >> 32u) & 0xffffu), static_cast(sizeof(Stored)), 0x01016facu}; + std::copy(storeBuffer.begin(), storeBuffer.end(), vertexUserData.begin() + 4); + 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, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} @@ -281,6 +291,18 @@ int main() { {"ONE + ONE_MINUS_SRC_ALPHA for color and alpha", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), false, [&](float r, float g) { return std::array{x + r * (1.0f - w), w + g * (1.0f - w)}; }}, }}; + { + Fill(block, layout); + const auto color = DecodeTarget(block); + bool rejected = false; + try { + Draw(*device, color, cases[0].blend, Program(exported), StoringVertexCode); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find("a draw whose shaders write memory") != std::string_view::npos; + if (!rejected) throw; + } + Require(rejected, "a split 8_8 SWAP_ALT draw whose vertex shader stores to memory was not rejected"); + } for (const auto& test : cases) { const auto before = Fill(block, layout); const auto color = DecodeTarget(block); From 8e5091c2f00090cb3def109fa50d56ba3c3ff010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 22:57:06 +0200 Subject: [PATCH 238/277] docs(technical-debt): name the SPIR-V rule behind keeping the loop back edge Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ac9abc9a3..46c5768df 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -105,7 +105,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws -- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge and the NVIDIA driver 615.78.08 crashes compiling such a loop; a loop whose back-edge block becomes unreachable in another way is not handled +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [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. From 6c7a2cbe695ed80e30f83c1ec2a5f752c0ccabfc Mon Sep 17 00:00:00 2001 From: Achraf Reyani Date: Sun, 11 Oct 2026 20:31:39 +0900 Subject: [PATCH 239/277] feat(libkernel): export _ioctl and getsid Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++++++ core/libs/prx/libkernel/File/src/Open.cpp | 9 +++++++ .../libs/prx/libkernel/System/src/Process.cpp | 6 +++++ core/libs/tests/GuestIoctlGetsid.cpp | 25 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 50 insertions(+) create mode 100644 core/libs/tests/GuestIoctlGetsid.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index db2752fa4..edce24d54 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -517,6 +517,15 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ioctl_getsid_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoctlGetsid.cpp) + target_include_directories(guest_ioctl_getsid_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ioctl_getsid_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_ioctl_getsid_tests) + add_test(NAME guest_ioctl_getsid COMMAND guest_ioctl_getsid_tests) + if(WIN32) + set_tests_properties(guest_ioctl_getsid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 93151b4d6..2acd24e4e 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -11,6 +11,7 @@ #include "SceTypes.hpp" #include +#include #include #include #include @@ -294,4 +295,12 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } +int APS5_VABI _ioctl_nid_postfix(int d, unsigned long request, ...) { + (void)d; + char name[40]; + std::snprintf(name, sizeof(name), "_ioctl: request 0x%lx", request); + NotImplemented_nid_no_patch(name); + return -1; +} + } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..152606d35 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -200,6 +200,12 @@ int APS5_VABI getpid_nid_postfix(void) { return static_cast(pid); } +int APS5_VABI getsid_nid_postfix(int pid) { + (void)pid; + NotImplemented_nid_no_patch("getsid: process sessions"); + return -1; +} + int APS5_VABI getppid_nid_postfix(void) { #ifdef _WIN32 const DWORD self = GetCurrentProcessId(); diff --git a/core/libs/tests/GuestIoctlGetsid.cpp b/core/libs/tests/GuestIoctlGetsid.cpp new file mode 100644 index 000000000..8a58fab3b --- /dev/null +++ b/core/libs/tests/GuestIoctlGetsid.cpp @@ -0,0 +1,25 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" { +int APS5_VABI _ioctl_nid_postfix(int, unsigned long, ...); +int APS5_VABI getsid_nid_postfix(int); +} +static void Require(bool value) { if (!value) std::abort(); } +template +static bool ReportsNotImplemented(Call call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} +int main() { + int value = 0; + Require(ReportsNotImplemented([&] { _ioctl_nid_postfix(0, 0x402c7413ul, &value); }, "_ioctl: request 0x402c7413 not implemented")); + Require(ReportsNotImplemented([&] { _ioctl_nid_postfix(3, 0x2000745eul); }, "_ioctl: request 0x2000745e not implemented")); + Require(ReportsNotImplemented([] { getsid_nid_postfix(0); }, "getsid: process sessions not implemented")); + Require(ReportsNotImplemented([] { getsid_nid_postfix(1); }, "getsid: process sessions not implemented")); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fee67c41b..26a9dc907 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -587,6 +587,7 @@ Throughout the project, every function at every stage either **does exactly what - [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`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied - [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)) - [sigtimedwait](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` +- [_ioctl](../../core/libs/prx/libkernel/File/src/Open.cpp) and [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throw, naming the ioctl request: no device control requests or process sessions are modelled. PPSA74126 imports both and fails to load without them. Its `_ioctl` call sites are the SDK libc's `termios` wrappers (`TIOCGETA`, `TIOCSETA`, `TIOCDRAIN`, `TIOCFLUSH` and the like); the title made no call to either in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, 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) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console From 76d43c525e42762f539c79f621861c1b57ff1422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 13:58:29 +0200 Subject: [PATCH 240/277] fix(libkernel): drop _ioctl, already exported by #3487 Co-authored-by: Achraf Reyani --- core/libs/prx/libkernel/File/src/Open.cpp | 9 --------- core/libs/tests/GuestIoctlGetsid.cpp | 4 ---- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 2acd24e4e..93151b4d6 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -11,7 +11,6 @@ #include "SceTypes.hpp" #include -#include #include #include #include @@ -295,12 +294,4 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } -int APS5_VABI _ioctl_nid_postfix(int d, unsigned long request, ...) { - (void)d; - char name[40]; - std::snprintf(name, sizeof(name), "_ioctl: request 0x%lx", request); - NotImplemented_nid_no_patch(name); - return -1; -} - } diff --git a/core/libs/tests/GuestIoctlGetsid.cpp b/core/libs/tests/GuestIoctlGetsid.cpp index 8a58fab3b..288e0a8c3 100644 --- a/core/libs/tests/GuestIoctlGetsid.cpp +++ b/core/libs/tests/GuestIoctlGetsid.cpp @@ -3,7 +3,6 @@ #include #include extern "C" { -int APS5_VABI _ioctl_nid_postfix(int, unsigned long, ...); int APS5_VABI getsid_nid_postfix(int); } static void Require(bool value) { if (!value) std::abort(); } @@ -17,9 +16,6 @@ static bool ReportsNotImplemented(Call call, const std::string& message) { return false; } int main() { - int value = 0; - Require(ReportsNotImplemented([&] { _ioctl_nid_postfix(0, 0x402c7413ul, &value); }, "_ioctl: request 0x402c7413 not implemented")); - Require(ReportsNotImplemented([&] { _ioctl_nid_postfix(3, 0x2000745eul); }, "_ioctl: request 0x2000745e not implemented")); Require(ReportsNotImplemented([] { getsid_nid_postfix(0); }, "getsid: process sessions not implemented")); Require(ReportsNotImplemented([] { getsid_nid_postfix(1); }, "getsid: process sessions not implemented")); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 26a9dc907..61db01afc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -587,7 +587,7 @@ Throughout the project, every function at every stage either **does exactly what - [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`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied - [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)) - [sigtimedwait](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` -- [_ioctl](../../core/libs/prx/libkernel/File/src/Open.cpp) and [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throw, naming the ioctl request: no device control requests or process sessions are modelled. PPSA74126 imports both and fails to load without them. Its `_ioctl` call sites are the SDK libc's `termios` wrappers (`TIOCGETA`, `TIOCSETA`, `TIOCDRAIN`, `TIOCFLUSH` and the like); the title made no call to either in a 60 s run +- [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, 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) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console From 1d5f6994390c890931f857181a4e865ee3fb1101 Mon Sep 17 00:00:00 2001 From: LightStorm72 Date: Sun, 11 Oct 2026 09:05:33 +0300 Subject: [PATCH 241/277] fix(libSceHttp): return an empty CA list from sceHttpsGetCaList/sceHttpsFreeCaList --- core/libs/prx/libSceHttp/Export.cpp | 20 ++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 3b9216532..faa16483d 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -22,6 +22,10 @@ struct RequestStatusRegistration { }; static std::mutex g_statusMutex; static std::map g_statusCallbacks; +struct HttpsCaList { + void* certs; + int certsNum; +}; extern "C" { @@ -438,4 +442,20 @@ int APS5_VABI sceHttpAbortWaitRequest(void) { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceHttpsGetCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index fee67c41b..1b45efeb3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -41,7 +41,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does -- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication +- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device From 7ca6b63905d7d2fc66bae96984b2f51971d14282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 14:03:08 +0200 Subject: [PATCH 242/277] docs(technical-debt): note the CA list stubs skip context validation Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1b45efeb3..5c9f9faa9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -41,7 +41,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does -- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title +- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device From cd814b8c6fb013e581313505b93b705299ce33d5 Mon Sep 17 00:00:00 2001 From: ivica00 Date: Sun, 11 Oct 2026 14:50:23 +0200 Subject: [PATCH 243/277] fix(shader): return 0 for an 8-bit sRGB bilinear cube comparison An 8-bit sRGB compare is already 0 for every texel, so the selected face cannot change the result. --- .../tests/execution/EmulatedColorCompare.cpp | 27 ++++++++++++++++--- .../Optimization/src/ResourceMaterializer.cpp | 3 ++- docs/dev/TechnicalDebt.md | 4 +-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index e53b5dc51..cc3452070 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -309,6 +309,24 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } +void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { + FillCubeTexels(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + CubeInput[tid * 4u + 0u] = 0.5f; + CubeInput[tid * 4u + 1u] = 1.0f; + CubeInput[tid * 4u + 2u] = 0.0f; + CubeInput[tid * 4u + 3u] = 0.0f; + } + Output.fill(-1.0f); + const auto result = Compile(device, format, sampler, CubeCode, false, true, true); + Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CubeCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); + } +} + void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { Output.fill(-1.0f); const auto result = Compile(device, format, sampler, Code, useCache); @@ -343,6 +361,12 @@ int main() { FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function); + RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + } + RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); + RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP"); Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats"); Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); @@ -363,9 +387,6 @@ int main() { } if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant); } - for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) { - Reject(*device, format, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); - } Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index b143b78d1..53118b192 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -551,7 +551,8 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; - if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u) { + const bool eightBitSrgb = format == IrBufferFormat::Format8Srgb || format == IrBufferFormat::Format8_8Srgb || format == IrBufferFormat::Format8_8_8_8Srgb; + if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u && !eightBitSrgb) { throw std::runtime_error("bilinear cube comparison requires DISABLE_CUBE_WRAP to avoid seamless face filtering"); } const auto addressMode = [](std::uint32_t clamp) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0ec9a30db..830b0077c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -64,7 +64,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet - [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select, move or phi is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 - [sceAudioPropagationSourceSetAudioPath](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. -- [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters and bilinear cube comparisons without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused. Bilinear cube comparisons of float, unorm and snorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified - [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfoList](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified - [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. @@ -536,7 +536,7 @@ Throughout the project, every function at every stage either **does exactly what - 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). - [Directed f16 rounding](../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. -- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255; that measurement also returns 0 for 8-bit UNORM, which this path still compares. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255; that measurement also returns 0 for 8-bit UNORM, which this path still compares. A bilinear cube of those 8-bit sRGB formats without `DISABLE_CUBE_WRAP` returns 0 for the same reason; float, unorm and snorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. From eeb647c6b54cb5f40faa612604d2e80cdeb91a4f Mon Sep 17 00:00:00 2001 From: bilel zenned <33394991+lisn0@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:01:33 +0100 Subject: [PATCH 244/277] test(libkernel): expose the missing named shared-memory path (#3921) Exercise named object creation, reopening, descriptor permissions, truncation and cross-process identity before adding Linux support. The standalone target compiles the production implementation without graphics dependencies. Tested: Source and staged diff reviewed; failing baseline will run on 191. Not-tested: Full runtime build and Windows. --- core/libs/tests/GuestNamedShmOpen.cpp | 98 ++++++++++++++++++++ core/libs/tests/shared_memory/CMakeLists.txt | 17 ++++ core/libs/tests/shared_memory/Support.cpp | 11 +++ 3 files changed, 126 insertions(+) create mode 100644 core/libs/tests/GuestNamedShmOpen.cpp create mode 100644 core/libs/tests/shared_memory/CMakeLists.txt create mode 100644 core/libs/tests/shared_memory/Support.cpp diff --git a/core/libs/tests/GuestNamedShmOpen.cpp b/core/libs/tests/GuestNamedShmOpen.cpp new file mode 100644 index 000000000..ab64c1549 --- /dev/null +++ b/core/libs/tests/GuestNamedShmOpen.cpp @@ -0,0 +1,98 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s (guest errno=%d)\n", message, *__error_nid_postfix()); + std::exit(1); + } +} + +struct Object { + std::string name; + std::string hostName = "/anyps5-shm-"; + explicit Object(std::string suffix) : name("/anyps5-test-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags, int mode = 0600) const { return shm_open_nid_postfix(name.c_str(), flags, mode); } +}; + +static void RequireError(const char* name, int flags, int error) { + *__error_nid_postfix() = 0; + Require(shm_open_nid_postfix(name, flags, 0600) == -1, "expected open failure"); + Require(*__error_nid_postfix() == error, "incorrect guest errno"); +} + +int main() { + constexpr int readWrite = 2, create = 0x200, exclusive = 0x800, truncate = 0x400; + Object object("/nested"); + RequireError(object.name.c_str(), readWrite, 2); + *__error_nid_postfix() = 73; + const int first = object.Open(readWrite | create | exclusive); + Require(first >= 0, "named creation failed"); + Require(*__error_nid_postfix() == 73, "successful creation overwrote guest errno"); + Require((fcntl(first, F_GETFD) & FD_CLOEXEC) != 0, "descriptor is not close-on-exec"); + struct stat info{}; + Require(fstat(first, &info) == 0 && info.st_size == 0, "new object is not empty"); + Require(ftruncate(first, 0x4000) == 0, "resize failed"); + auto* mapped = static_cast(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, first, 0)); + Require(mapped != MAP_FAILED, "host shared mapping failed"); + std::memcpy(mapped, "shared", 7); + RequireError(object.name.c_str(), readWrite | create | exclusive, 17); + const int reader = object.Open(truncate); + Require(reader >= 0, "read-only reopen failed"); + Require(fstat(reader, &info) == 0 && info.st_size == 0x4000, "read-only O_TRUNC truncated the object"); + char bytes[7]{}; + Require(pread(reader, bytes, sizeof(bytes), 0) == sizeof(bytes) && std::memcmp(bytes, "shared", 7) == 0, "reopened object has different contents"); + Require(pwrite(reader, bytes, sizeof(bytes), 0) == -1, "read-only descriptor accepted a write"); + Require(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, reader, 0) == MAP_FAILED, "read-only descriptor accepted a writable shared mapping"); + const auto child = fork(); + Require(child >= 0, "fork failed"); + if (child == 0) { + const int reopened = object.Open(readWrite); + char contents[7]{}; + const bool okay = reopened >= 0 && pread(reopened, contents, sizeof(contents), 0) == sizeof(contents) && std::memcmp(contents, "shared", 7) == 0; + if (reopened >= 0) close(reopened); + _exit(okay ? 0 : 1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, "cross-process reopen failed"); + Require(::munmap(mapped, 0x4000) == 0, "host unmap failed"); + Require(close(reader) == 0 && close(first) == 0, "close failed"); + const int truncated = object.Open(readWrite | truncate); + Require(truncated >= 0 && fstat(truncated, &info) == 0 && info.st_size == 0, "read-write truncation failed"); + Require(close(truncated) == 0, "close after truncation failed"); + RequireError(nullptr, readWrite, 14); + RequireError("", readWrite, 22); + RequireError("relative", readWrite, 22); + RequireError(object.name.c_str(), 1, 22); + RequireError(object.name.c_str(), readWrite | 8, 22); + const std::string tooLong(1024, '/'); + RequireError(tooLong.c_str(), readWrite, 63); + Object longName(std::string(900, 'x')); + const auto previousMask = umask(0077); + const int longDescriptor = longName.Open(create | readWrite, 07777); + umask(previousMask); + Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed"); + Require((info.st_mode & 07777) == 0700, "mode or umask was not applied"); + Require(close(longDescriptor) == 0, "long name close failed"); + std::puts("named shared-memory checks passed"); +} diff --git a/core/libs/tests/shared_memory/CMakeLists.txt b/core/libs/tests/shared_memory/CMakeLists.txt new file mode 100644 index 000000000..6fe63c09b --- /dev/null +++ b/core/libs/tests/shared_memory/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.22) +project(SharedMemoryTests LANGUAGES CXX) +enable_testing() +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "Standalone shared-memory tests require Linux") +endif() +set(LIBS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../..") +add_executable(guest_named_shm_open_tests + ../GuestNamedShmOpen.cpp + Support.cpp + ${LIBS_DIR}/prx/libkernel/File/src/SharedMemory.cpp + ${LIBS_DIR}/nid/src/Sha1.cpp) +target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_DIR} ${LIBS_DIR}/nid/include) +target_compile_features(guest_named_shm_open_tests PRIVATE cxx_std_20) +target_link_libraries(guest_named_shm_open_tests PRIVATE rt) +add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) +set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) diff --git a/core/libs/tests/shared_memory/Support.cpp b/core/libs/tests/shared_memory/Support.cpp new file mode 100644 index 000000000..03f202e4e --- /dev/null +++ b/core/libs/tests/shared_memory/Support.cpp @@ -0,0 +1,11 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" int* APS5_VABI __error_nid_postfix() { + static thread_local int error; + return &error; +} + +extern "C" void NotImplemented_nid_no_patch(const char* name) { + throw std::runtime_error(name); +} From f274ceb18d06b90ff2ca02a7257124f58432554d Mon Sep 17 00:00:00 2001 From: bilel zenned <33394991+lisn0@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:06:10 +0100 Subject: [PATCH 245/277] feat(libkernel): allow Linux guests to reopen named shared memory (#3921) Translate guest named-open flags and permissions to POSIX shared-memory descriptors. Bound guest names and use the existing digest implementation to keep a stable AnyPS5 namespace, including long names and internal slashes. Preserve anonymous and Windows behavior, and record the separate unlink and guest mapping gaps. Constraint: Builds and tests run only through the enrolled 191 compute service. Rejected: Direct Linux guest names | cannot represent FreeBSD internal slashes and 1023-byte names. Tested: Source review and diff checks; baseline job100619 accepted, result retrieval currently times out. Not-tested: Runtime verification pending on191; full build, Windows and titles. Confidence: medium Scope-risk: narrow --- core/libs/prx/libkernel/CMakeLists.txt | 7 +++ .../prx/libkernel/File/src/SharedMemory.cpp | 62 ++++++++++++++++++- core/libs/tests/GuestNamedShmOpen.cpp | 38 ++++++++++-- core/libs/tests/GuestShmOpen.cpp | 4 ++ docs/dev/TechnicalDebt.md | 2 + 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 420493229..0322c6e7d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -755,6 +755,13 @@ if(BUILD_TESTING) target_link_libraries(guest_shm_open_tests PRIVATE libkernel libc) configure_windows_unwind(guest_shm_open_tests) add_test(NAME guest_shm_open COMMAND guest_shm_open_tests) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_named_shm_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmOpen.cpp) + target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_open_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) + set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) + endif() if(WIN32) set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 94bb3ee39..d522ef2b9 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -14,6 +14,8 @@ #include #include #else +#include "nid/Sha1.hpp" +#include #include #endif @@ -48,6 +50,56 @@ int GuestError(int error) { } } +#ifndef _WIN32 +int NamedGuestError(int error) { + switch (error) { + case ENOENT: + return 2; + case EINTR: + return 4; + case EACCES: + return 13; + case EFAULT: + return 14; + case EEXIST: + return 17; + case EINVAL: + return GuestInvalid; + case ENOSPC: + return 28; + case EROFS: + return 30; + case ENAMETOOLONG: + return 63; + default: + return GuestError(error); + } +} + +int OpenNamedFile(const char* path, int flags, int mode) { + if (path == nullptr) return Fail(14); + std::size_t length = 0; + while (length < 1024 && path[length] != '\0') ++length; + if (length == 1024) return Fail(63); + if (length == 0 || path[0] != '/') return Fail(GuestInvalid); + const auto digest = Nid::Sha1(std::vector(path, path + length)); + std::string hostName = "/anyps5-shm-"; + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + int hostFlags = (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR ? O_RDWR : O_RDONLY; + if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; + if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; + if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; + const int descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, mode & 0777); + if (descriptor >= 0) return descriptor; + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: opening the host named object failed, errno=" + std::to_string(error)); +} +#endif + int CreateAnonymousFile() { #ifdef _WIN32 static std::atomic sequence{0}; @@ -74,11 +126,17 @@ int CreateAnonymousFile() { } -extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int) { +extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int mode) { const int accessMode = flags & SCE_KERNEL_O_ACCMODE; if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); - if (path != anonymousObject) NotImplemented_nid_no_patch("shm_open of a named object"); + if (path != anonymousObject) { +#ifdef _WIN32 + NotImplemented_nid_no_patch("shm_open of a named object"); +#else + return OpenNamedFile(path, flags, mode); +#endif + } if (accessMode == SCE_KERNEL_O_RDONLY) return Fail(GuestInvalid); const int descriptor = CreateAnonymousFile(); if (descriptor >= 0) return descriptor; diff --git a/core/libs/tests/GuestNamedShmOpen.cpp b/core/libs/tests/GuestNamedShmOpen.cpp index ab64c1549..ecc39d164 100644 --- a/core/libs/tests/GuestNamedShmOpen.cpp +++ b/core/libs/tests/GuestNamedShmOpen.cpp @@ -1,9 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "nid/Sha1.hpp" #include -#include #include #include +#include #include #include #include @@ -15,8 +15,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); static void Require(bool value, const char* message) { if (!value) { - std::fprintf(stderr, "%s (guest errno=%d)\n", message, *__error_nid_postfix()); - std::exit(1); + throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); } } @@ -41,7 +40,7 @@ static void RequireError(const char* name, int flags, int error) { Require(*__error_nid_postfix() == error, "incorrect guest errno"); } -int main() { +static void Run() { constexpr int readWrite = 2, create = 0x200, exclusive = 0x800, truncate = 0x400; Object object("/nested"); RequireError(object.name.c_str(), readWrite, 2); @@ -64,6 +63,12 @@ int main() { Require(pread(reader, bytes, sizeof(bytes), 0) == sizeof(bytes) && std::memcmp(bytes, "shared", 7) == 0, "reopened object has different contents"); Require(pwrite(reader, bytes, sizeof(bytes), 0) == -1, "read-only descriptor accepted a write"); Require(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, reader, 0) == MAP_FAILED, "read-only descriptor accepted a writable shared mapping"); + Object separate("/different"); + const int different = separate.Open(readWrite | create | exclusive, 0400); + Require(different >= 0 && fstat(different, &info) == 0 && info.st_size == 0, "different names share an object"); + Require(ftruncate(different, 0x4000) == 0 && pwrite(different, "y", 1, 0) == 1, "creation permissions incorrectly restrict the new descriptor"); + Require(mapped[0] == 's', "different object write changed the first object"); + Require(close(different) == 0, "different object close failed"); const auto child = fork(); Require(child >= 0, "fork failed"); if (child == 0) { @@ -87,12 +92,35 @@ int main() { RequireError(object.name.c_str(), readWrite | 8, 22); const std::string tooLong(1024, '/'); RequireError(tooLong.c_str(), readWrite, 63); - Object longName(std::string(900, 'x')); + const auto prefixLength = std::string("/anyps5-test-").size() + std::to_string(getpid()).size(); + Object longName(std::string(1023 - prefixLength, 'x')); + Require(longName.name.size() == 1023, "incorrect boundary test length"); const auto previousMask = umask(0077); const int longDescriptor = longName.Open(create | readWrite, 07777); umask(previousMask); Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed"); Require((info.st_mode & 07777) == 0700, "mode or umask was not applied"); Require(close(longDescriptor) == 0, "long name close failed"); + const int longReader = longName.Open(0); + Require(longReader >= 0 && close(longReader) == 0, "maximum-length name reopen failed"); + const auto* anonymous = reinterpret_cast(1); + const int anonymousFirst = shm_open_nid_postfix(anonymous, readWrite, 0); + const int anonymousSecond = shm_open_nid_postfix(anonymous, readWrite | create | exclusive | truncate, 0400); + Require(anonymousFirst >= 0 && anonymousSecond >= 0, "anonymous creation regressed"); + Require(ftruncate(anonymousFirst, 0x4000) == 0 && pwrite(anonymousFirst, "x", 1, 0) == 1, "anonymous write failed"); + Require(pread(anonymousSecond, bytes, 1, 0) == 0, "anonymous objects share contents"); + Require(close(anonymousFirst) == 0 && close(anonymousSecond) == 0, "anonymous close failed"); + RequireError(anonymous, 0, 22); + RequireError(anonymous, 3, 22); std::puts("named shared-memory checks passed"); } + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp index 112ae3c90..357f6445a 100644 --- a/core/libs/tests/GuestShmOpen.cpp +++ b/core/libs/tests/GuestShmOpen.cpp @@ -47,6 +47,7 @@ int main() { RequireInvalid(anonymous, 0x2 | 0x4); RequireInvalid("/anyps5-shm", 0x1); RequireInvalid("/anyps5-shm", 0x202 | 0x8); +#ifdef _WIN32 bool threw = false; try { shm_open_nid_postfix("/anyps5-shm", 0x202, 0600); @@ -54,4 +55,7 @@ int main() { threw = true; } Require(threw); +#else + RequireInvalid("", 0x202); +#endif } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f5042d53e..8ada18bc4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -1,5 +1,7 @@ # Project technical debt +- Linux [shm_open](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) maps slash-prefixed guest names of up to 1023 bytes to `/anyps5-shm-` plus the existing SHA-1 digest. The namespace is shared across AnyPS5 processes in the host mount namespace; digest collisions are not detected. Named objects persist until host unlink or reboot because guest `shm_unlink` is absent. Named objects on Windows still throw. File-backed guest `mmap` is unsupported; descriptor sharing is tested with native Linux mappings. Guest invalid pointers other than null cannot be reported safely as `EFAULT`. Console behaviour and title compatibility remain unverified. + ### Build - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) From 2aaeaa3f050cf11ac4a9dda0f04244b303b2c651 Mon Sep 17 00:00:00 2001 From: bilel Date: Sun, 11 Oct 2026 12:53:27 +0100 Subject: [PATCH 246/277] feat(libkernel): make Linux shared memory usable through guest APIs (#3923) Complete the object lifecycle through guest mapping and unlink APIs, and preserve FreeBSD shared-object size and seek rules across scalar, vector and asynchronous descriptor writes. Pin descriptors during operations and coordinate size-sensitive operations across cooperating emulator processes. Constraint: Linux host support with existing dependencies and allocation tracking Rejected: Bare descriptor registry | close, duplication and reuse invalidate identity Rejected: Per-object OFD locks | forked descriptors share the same lock description Confidence: high Scope-risk: moderate Directive: Keep guest resize, truncating open and bounded writes under the same namespace guard Tested: Ten Linux runtime suites; focused shared-memory ASan/UBSan suite with warnings as errors Not-tested: Windows build, PS5 hardware and title compatibility --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 4 +- core/libs/prx/libkernel/CMakeLists.txt | 10 + .../libkernel/DirectMemory/DirectMemory.cpp | 40 ++- .../libkernel/DirectMemory/DirectMemory.hpp | 1 + .../libkernel/DirectMemory/PosixMemory.cpp | 66 +++- .../libkernel/File/include/SharedMemory.hpp | 24 ++ core/libs/prx/libkernel/File/src/Open.cpp | 13 +- .../prx/libkernel/File/src/SharedMemory.cpp | 330 +++++++++++++++++- core/libs/prx/libkernel/File/src/Stdio.cpp | 38 +- core/libs/tests/GuestNamedShmConcurrency.cpp | 167 +++++++++ core/libs/tests/GuestNamedShmLifecycle.cpp | 211 +++++++++++ core/libs/tests/GuestShmOpen.cpp | 4 + docs/dev/TechnicalDebt.md | 3 +- 13 files changed, 877 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/SharedMemory.hpp create mode 100644 core/libs/tests/GuestNamedShmConcurrency.cpp create mode 100644 core/libs/tests/GuestNamedShmLifecycle.cpp diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 040773e22..1889e097f 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #ifndef _WIN32 @@ -76,7 +77,8 @@ std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, s } return NativePositioned_nid_no_patch(fd, const_cast(buf), nbyte, offset, true); #else - return static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(fd, buf, nbyte, offset); + return result == -2 ? static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))) : result; #endif } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 0322c6e7d..8bd7c4c86 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -761,6 +761,16 @@ if(BUILD_TESTING) target_link_libraries(guest_named_shm_open_tests PRIVATE libkernel libc rt) add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmLifecycle.cpp) + target_include_directories(guest_named_shm_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_lifecycle_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_lifecycle COMMAND guest_named_shm_lifecycle_tests) + set_tests_properties(guest_named_shm_lifecycle PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp) + target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_concurrency COMMAND guest_named_shm_concurrency_tests) + set_tests_properties(guest_named_shm_concurrency PROPERTIES TIMEOUT 20) endif() if(WIN32) set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 1fae6ec14..afa6d4420 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -673,7 +673,6 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { std::mutex g_reservationLock; std::map g_reservations; - void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast(addr); const auto end = start + len; @@ -830,6 +829,45 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment) { return 0; } +int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment) { + ValidateOutput(addr); + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || (offset & (PS5_PAGE_SIZE - 1)) != 0 || len > static_cast(std::numeric_limits::max()) || offset > static_cast(std::numeric_limits::max()) - len) return SCE_KERNEL_ERROR_EINVAL; +#if defined(__linux__) + const bool shared = (flags & 3) == 1; + GuestAllocations::Mutation mutation; + const int placementFlags = flags & (GuestMapFixedFlag | 0x80 | 0x400000); + flags = PlaceHintInReservation(*addr, len, placementFlags); + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; + ReplaceFixedOverlap(mutation, *addr, len, flags); + void* mapped = MapPlaced(*addr, len, PROT_NONE, flags, alignment); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(mapped, len); + const int nativeProtection = LinuxProtFromSce(prot); + const int nativeFlags = (shared ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED; + if (::mmap(mapped, len, nativeProtection, nativeFlags, descriptor, static_cast(offset)) == MAP_FAILED) { + const int error = errno; + Unmap(mapped, len); + throw std::system_error(error, std::generic_category(), "map guest shared memory"); + } + try { + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + Unmap(mapped, len); + throw; + } + *addr = mapped; + EraseReservations(mapped, len); + RecordProtection(mapped, len, prot); + Trace("map file %p+0x%zx offset=0x%llx prot=0x%x flags=0x%x", mapped, len, static_cast(offset), prot, flags); + return 0; +#else + static_cast(prot); + static_cast(flags); + static_cast(descriptor); + static_cast(offset); + throw std::system_error(ENOTSUP, std::generic_category(), "file-backed guest mmap is not supported on this host"); +#endif +} + #ifdef _WIN32 static bool IsGuestModuleImage(const void* base) { std::wstring path(MAX_PATH, L'\0'); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 591dabb17..e6d1a5a60 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -30,6 +30,7 @@ bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alig size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE); +int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); int DoMprotect(const void* addr, size_t len, int prot); int DoMtypeprotect(const void* addr, size_t len, int type, int prot); int DoMunmap(void* addr, size_t len); diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 650734b5e..94bdad8af 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -2,9 +2,16 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include +#include #include #include +#include +#if defined(__linux__) +#include +#include +#endif extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length); @@ -15,9 +22,16 @@ namespace { // FreeBSD/PS5 ABI values, independent of the host's errno and mmap constants. constexpr int GuestInvalid = 22; constexpr int GuestNoMemory = 12; +constexpr int GuestBadDescriptor = 9; +constexpr int GuestAccess = 13; +constexpr int GuestIoError = 5; constexpr int GuestNotSupported = 45; +constexpr int GuestShared = 0x1; constexpr int GuestPrivate = 0x2; constexpr int GuestAnonymous = 0x1000; +constexpr int GuestMapFixed = 0x10; +constexpr int GuestMapNoOverwrite = 0x80; +constexpr int GuestMapNoCoalesce = 0x400000; constexpr int GuestSyncAsync = 0x1; constexpr int GuestSyncInvalidate = 0x2; constexpr int GuestAdviceCore = 9; @@ -55,17 +69,54 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti std::size_t rounded; if (!RoundLength(length, rounded) || (protection & ~7) != 0) return failed(GuestInvalid); - if ((flags & ~GuestAlignedSuper) != (GuestPrivate | GuestAnonymous)) + const int mappingFlags = flags & ~GuestAlignedSuper; + if (mappingFlags == (GuestPrivate | GuestAnonymous)) { + if (descriptor != -1 || offset != 0) return failed(GuestInvalid); + void* mapped = nullptr; + try { + const auto result = DoMapAnon(&mapped, rounded, protection, 0, (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE); + if (result != 0) return failed(GuestNoMemory); + return mapped; + } catch (const std::bad_alloc&) { + return failed(GuestNoMemory); + } catch (const std::exception&) { + return failed(GuestNoMemory); + } + } + const int kind = mappingFlags & (GuestShared | GuestPrivate); + int placement = mappingFlags & (GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce); + if ((mappingFlags & GuestAnonymous) != 0 || + (kind != GuestShared && kind != GuestPrivate) || + (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); - if (descriptor != -1 || offset != 0) + if (descriptor < 0) return failed(GuestBadDescriptor); + if (offset < 0 || (static_cast(offset) & (PS5_PAGE_SIZE - 1)) != 0 || + rounded > static_cast(std::numeric_limits::max()) || + static_cast(offset) > static_cast(std::numeric_limits::max()) - rounded) return failed(GuestInvalid); - // A non-fixed address is a hint; the kernel may choose another address. - (void)address; +#if defined(__linux__) + const int descriptorFlags = fcntl(descriptor, F_GETFL); + if (descriptorFlags < 0) return failed(errno); + const int accessMode = descriptorFlags & O_ACCMODE; + if ((protection & 1) != 0 && accessMode == O_WRONLY) return failed(GuestAccess); + if (kind == GuestShared && (protection & 2) != 0 && accessMode == O_RDONLY) return failed(GuestAccess); +#endif + if (!File::IsSharedMemoryDescriptor_nid_no_patch(descriptor)) return failed(GuestNotSupported); + const std::size_t alignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; void* mapped = nullptr; try { - const auto result = DoMapAnon(&mapped, rounded, protection, 0, (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE); - if (result != 0) return failed(GuestNoMemory); + mapped = address; + const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, descriptor, static_cast(offset), alignment); + if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); return mapped; + } catch (const std::invalid_argument&) { + return failed(GuestInvalid); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + if (hostError == ENOTSUP || hostError == EOPNOTSUPP) return failed(GuestNotSupported); + if (hostError == EACCES || hostError == EPERM) return failed(GuestAccess); + if (hostError == EBADF) return failed(GuestBadDescriptor); + return failed(hostError > 0 && hostError <= 34 ? hostError : GuestIoError); } catch (const std::bad_alloc&) { return failed(GuestNoMemory); } catch (const std::exception&) { @@ -109,6 +160,9 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + return failed(hostError == EACCES || hostError == EPERM ? GuestAccess : GuestInvalid); } catch (const std::exception&) { return failed(GuestInvalid); } diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp new file mode 100644 index 000000000..5c9ffc9f4 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -0,0 +1,24 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP + +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence); +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length); +#ifndef _WIN32 +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count); +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset); +#endif + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 66250bf90..e6d28c4e0 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -25,7 +26,8 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return ::_wopen(p.wstring().c_str(), nativeFlags, static_cast(mode)); } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::_lseeki64(fd, offset, whence); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + return result == -2 ? ::_lseeki64(fd, offset, whence) : result; } static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { @@ -37,7 +39,8 @@ 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"); } - return ::_write(fd, buf, static_cast(n)); + const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + return result == -2 ? ::_write(fd, buf, static_cast(n)) : static_cast(result); } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} @@ -79,13 +82,15 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return ::open(p.c_str(), nativeFlags, static_cast(mode)); } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::lseek(fd, static_cast(offset), whence); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + return result == -2 ? ::lseek(fd, static_cast(offset), whence) : result; } static std::int64_t NativeRead(int fd, void* buf, std::size_t n) { return ::read(fd, buf, n); } static std::int64_t NativeWrite(int fd, const void* buf, std::size_t n) { - return ::write(fd, buf, n); + const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + return result == -2 ? ::write(fd, buf, n) : result; } static int NativeClose(int fd) { return ::close(fd); } static int NativeUnlink(const std::filesystem::path& p) { diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index d522ef2b9..25d157204 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -1,9 +1,18 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libkernel/File/include/File.hpp" #include +#include +#include +#include #include #include +#include +#include +#include +#include #ifdef _WIN32 #include @@ -17,12 +26,60 @@ #include "nid/Sha1.hpp" #include #include +#include +#include +#include +#include +#include #endif extern "C" int* APS5_VABI __error_nid_postfix(); namespace { +bool IsHostSharedMemoryDescriptor(int descriptor) { +#ifdef __linux__ + char descriptorPath[64]; + std::snprintf(descriptorPath, sizeof(descriptorPath), "/proc/self/fd/%d", descriptor); + char objectPath[4096]; + const auto length = ::readlink(descriptorPath, objectPath, sizeof(objectPath) - 1); + if (length < 0) return false; + objectPath[length] = '\0'; + return std::string_view(objectPath).starts_with("/dev/shm/anyps5-shm-") || + std::string_view(objectPath).starts_with("/memfd:anyps5 shm_open"); +#else + (void)descriptor; + return false; +#endif +} + +class HostShmLock { +public: + HostShmLock() { +#ifdef __linux__ + descriptor = ::open("/dev/shm", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (descriptor < 0 || ::flock(descriptor, LOCK_EX) != 0) error = errno; +#endif + } + + ~HostShmLock() { +#ifdef __linux__ + const int savedError = errno; + if (descriptor >= 0) { + if (!error) ::flock(descriptor, LOCK_UN); + ::close(descriptor); + } + errno = savedError; +#endif + } + + int Error() const { return error; } + +private: + int descriptor = -1; + int error = 0; +}; + constexpr int GuestNoMemory = 12; constexpr int GuestInvalid = 22; constexpr int GuestTooManyFilesInSystem = 23; @@ -76,24 +133,40 @@ int NamedGuestError(int error) { } } -int OpenNamedFile(const char* path, int flags, int mode) { - if (path == nullptr) return Fail(14); +std::string NamedHostPath(const char* path, int& error) { + if (path == nullptr) { error = EFAULT; return {}; } std::size_t length = 0; while (length < 1024 && path[length] != '\0') ++length; - if (length == 1024) return Fail(63); - if (length == 0 || path[0] != '/') return Fail(GuestInvalid); + if (length == 1024) { error = ENAMETOOLONG; return {}; } + if (length == 0 || path[0] != '/') { error = EINVAL; return {}; } const auto digest = Nid::Sha1(std::vector(path, path + length)); std::string hostName = "/anyps5-shm-"; for (const auto byte : digest) { hostName += "0123456789abcdef"[byte >> 4]; hostName += "0123456789abcdef"[byte & 15]; } + error = 0; + return hostName; +} + +int OpenNamedFile(const char* path, int flags, int mode) { + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_open: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } int hostFlags = (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR ? O_RDWR : O_RDONLY; if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; const int descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, mode & 0777); - if (descriptor >= 0) return descriptor; + if (descriptor >= 0) { + return descriptor; + } const int error = errno; if (const int guest = NamedGuestError(error)) return Fail(guest); throw std::runtime_error("shm_open: opening the host named object failed, errno=" + std::to_string(error)); @@ -120,12 +193,230 @@ int CreateAnonymousFile() { errno = EEXIST; return -1; #else - return memfd_create("anyps5 shm_open", MFD_CLOEXEC); + const int descriptor = memfd_create("anyps5 shm_open", MFD_CLOEXEC); + return descriptor; #endif } } +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { + return IsHostSharedMemoryDescriptor(descriptor); +} + +namespace { + +class SharedMemoryLock { +public: + explicit SharedMemoryLock(int value) { + if (!IsHostSharedMemoryDescriptor(value)) return; + shared = true; + hostLock.emplace(); + if (hostLock->Error() != 0) { error = hostLock->Error(); return; } + descriptor = Duplicate(value); + if (descriptor < 0) { error = errno; return; } + if (!IsHostSharedMemoryDescriptor(descriptor)) { error = EBADF; return; } + if (!Refresh()) error = errno; + } + + ~SharedMemoryLock() { + if (descriptor >= 0) { + const int savedError = errno; +#ifdef _WIN32 + ::_close(descriptor); +#else + ::close(descriptor); +#endif + errno = savedError; + } + } + + bool IsShared() const { return shared; } + int Error() const { return error; } + int Descriptor() const { return descriptor; } + bool Refresh() { +#ifdef _WIN32 + struct _stat64 nativeStatus{}; + if (::_fstat64(descriptor, &nativeStatus) != 0) return false; + status.st_size = nativeStatus.st_size; + return true; +#else + return ::fstat(descriptor, &status) == 0; +#endif + } + struct stat status{}; + +private: + static int Duplicate(int value) { +#ifdef _WIN32 + return ::_dup(value); +#else + return ::fcntl(value, F_DUPFD_CLOEXEC, 0); +#endif + } + + int descriptor = -1; + std::optional hostLock; + bool shared = false; + int error = 0; +}; +} + +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; +#ifdef _WIN32 + const auto position = ::_lseeki64(fd, 0, SEEK_CUR); +#else + const auto position = ::lseek(fd, 0, SEEK_CUR); +#endif + if (position < 0) return -1; + if (position >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - position)); +#ifdef _WIN32 + const auto result = ::_write(fd, buffer, static_cast(std::min(count, std::numeric_limits::max()))); +#else + const auto result = ::write(fd, buffer, static_cast(count)); +#endif + return static_cast(result); +} + +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; + if (offset < 0) { errno = EINVAL; return -1; } + if (offset >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - offset)); +#ifdef _WIN32 + const auto result = NativePositioned_nid_no_patch(fd, const_cast(buffer), static_cast(count), offset, true); +#else + const auto result = ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); +#endif + return static_cast(result); +} + +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); + const auto& status = guard.status; + std::int64_t base = 0; + if (whence == SEEK_CUR) { +#ifdef _WIN32 + base = ::_lseeki64(fd, 0, SEEK_CUR); +#else + base = ::lseek(fd, 0, SEEK_CUR); +#endif + if (base < 0) return -1; + } else if (whence == SEEK_END) { + base = static_cast(status.st_size); + } else if (whence != SEEK_SET) { + errno = EINVAL; + return -1; + } + if ((offset > 0 && base > std::numeric_limits::max() - offset) || + (offset < 0 && base < std::numeric_limits::min() - offset)) { + errno = EOVERFLOW; + return -1; + } + const auto target = base + offset; + if (target < 0 || target > status.st_size) { + errno = EINVAL; + return -1; + } +#ifdef _WIN32 + return ::_lseeki64(fd, target, SEEK_SET); +#else + return ::lseek(fd, static_cast(target), SEEK_SET); +#endif +} + +#ifndef _WIN32 +static std::int64_t TransferSharedMemoryVectors(int descriptor, const struct iovec* vectors, int count, + std::int64_t offset, bool positioned) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } + if (positioned && offset < 0) { errno = EINVAL; return -1; } + struct stat status{}; + if (::fstat(fd, &status) != 0) return -1; + std::uint64_t total = 0; + constexpr auto maxTransfer = static_cast(std::numeric_limits::max()); + for (int i = 0; i < count; ++i) { + if (vectors[i].iov_len > maxTransfer - total) { errno = EINVAL; return -1; } + total += vectors[i].iov_len; + } + std::uint64_t available = 0; + if (positioned) { + if (offset < status.st_size) available = static_cast(status.st_size - offset); + if (total > static_cast(std::numeric_limits::max() - offset)) { errno = EINVAL; return -1; } + } else { + const auto position = ::lseek(fd, 0, SEEK_CUR); + if (position < 0) return -1; + if (position < status.st_size) available = static_cast(status.st_size - position); + } + std::vector bounded; + bounded.reserve(static_cast(count)); + for (int i = 0; i < count && available > 0; ++i) { + const auto length = static_cast(std::min(vectors[i].iov_len, available)); + if (length != 0) bounded.push_back({vectors[i].iov_base, length}); + available -= length; + } + if (bounded.empty()) return 0; + return positioned ? ::pwritev(fd, bounded.data(), static_cast(bounded.size()), static_cast(offset)) + : ::writev(fd, bounded.data(), static_cast(bounded.size())); +} + +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count) { + return TransferSharedMemoryVectors(descriptor, vectors, count, 0, false); +} + +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset) { + return TransferSharedMemoryVectors(descriptor, vectors, count, offset, true); +} +#endif + +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); +#ifdef _WIN32 + const int result = static_cast(::_chsize_s(fd, length)); +#else + const int result = ::ftruncate(fd, static_cast(length)); +#endif + return result; +} + +} + extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int mode) { const int accessMode = flags & SCE_KERNEL_O_ACCMODE; if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); @@ -144,3 +435,30 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m if (const int guest = GuestError(error)) return Fail(guest); throw std::runtime_error("shm_open: creating the host anonymous file failed, errno=" + std::to_string(error)); } + +extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { +#ifdef _WIN32 + (void)path; + NotImplemented_nid_no_patch("shm_unlink of a named object"); +#else + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_unlink: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } + const auto objectPath = "/dev/shm" + hostName; + if (::faccessat(AT_FDCWD, objectPath.c_str(), R_OK | W_OK, AT_EACCESS) != 0) { + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: checking shared-memory permissions failed, errno=" + std::to_string(error)); + } + if (::shm_unlink(hostName.c_str()) == 0) return 0; + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: removing the host named object failed, errno=" + std::to_string(error)); +#endif +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 64db9b028..54e106dff 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -7,6 +7,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" @@ -119,7 +120,8 @@ static int NativeFchmod(int descriptor, int mode) { return path ? NativeChmod(*path, mode) : -1; } static int NativeFtruncate(int descriptor, std::int64_t length) { - return static_cast(::_chsize_s(descriptor, length)); + const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); + return result == -2 ? static_cast(::_chsize_s(descriptor, length)) : result; } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -187,6 +189,10 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); } + if (write) { + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + if (result != -2) return result; + } const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; @@ -218,6 +224,10 @@ 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"); } + if (write) { + const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); + if (result != -2) return result; + } return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); } static bool NativeIsDisk(int descriptor) { @@ -255,7 +265,8 @@ static int NativeFchmod(int descriptor, int mode) { return ::fchmod(descriptor, static_cast(mode)); } static int NativeFtruncate(int descriptor, std::int64_t length) { - return ::ftruncate(descriptor, static_cast(length)); + const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); + return result == -2 ? ::ftruncate(descriptor, static_cast(length)) : result; } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { return ::truncate(path.c_str(), static_cast(length)) == 0 ? 0 : errno; @@ -279,7 +290,8 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); } static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { - return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + return result == -2 ? static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))) : result; } static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); @@ -652,18 +664,12 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const 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; - auto n = NativePwrite(d, buf, nbytes, offset); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pwrite failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + char empty = 0; + const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { @@ -989,6 +995,8 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); + if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1008,6 +1016,8 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in if (const int error = CheckIovecs(iov, iovcnt)) return error; if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); + if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); return result < 0 ? SceErrorFromErrno(errno) : result; } diff --git a/core/libs/tests/GuestNamedShmConcurrency.cpp b/core/libs/tests/GuestNamedShmConcurrency.cpp new file mode 100644 index 000000000..60d91794a --- /dev/null +++ b/core/libs/tests/GuestNamedShmConcurrency.cpp @@ -0,0 +1,167 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI fchmod_nid_postfix(int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +using Unlink = int (*)(const char*); +constexpr std::int64_t Page = 0x4000; +constexpr int ReadWriteCreate = 0xa02; + +struct Object { + std::string name = "/anyps5-concurrent-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } +}; + +bool Race(int descriptor) { + for (int iteration = 0; iteration < 500; ++iteration) { + const auto size = (iteration & 1) ? 0 : Page; + if (ftruncate_nid_postfix(descriptor, size) != 0) return false; + const auto written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page / 2); + if (written < 0) return false; + FileStat info{}; + if (fstat_nid_disambig1_nid_postfix(descriptor, &info) != 0 || + (info.st_size != 0 && info.st_size != Page)) return false; + } + return true; +} + +bool NoDescriptorFallback(int descriptor) { + if (ftruncate_nid_postfix(descriptor, Page) != 0) return false; + const pid_t child = fork(); + if (child < 0) return false; + if (child == 0) { + const struct rlimit limit{0, 0}; + if (setrlimit(RLIMIT_NOFILE, &limit) != 0) _exit(2); + std::int64_t written = -2; + try { + written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page); + } catch (const std::exception& exception) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw=%s guest_errno=%d native_errno=%d\n", + descriptor, exception.what(), *__error_nid_postfix(), errno); + _exit(3); + } catch (...) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw unknown exception guest_errno=%d native_errno=%d\n", + descriptor, *__error_nid_postfix(), errno); + _exit(3); + } + const int guestError = *__error_nid_postfix(); + const int nativeError = errno; + struct stat info{}; + if (::fstat(descriptor, &info) != 0) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d fstat_errno=%d\n", + descriptor, static_cast(written), guestError, nativeError, errno); + _exit(4); + } + if (written != 0 && !(written == -1 && guestError == 24)) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld\n", + descriptor, static_cast(written), guestError, nativeError, static_cast(info.st_size)); + _exit(5); + } + if (info.st_size != Page) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld expected=%lld\n", + descriptor, static_cast(written), guestError, nativeError, + static_cast(info.st_size), static_cast(Page)); + _exit(6); + } + _exit(0); + } + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +bool ChildrenSucceeded(const pid_t* children, int count) { + bool okay = true; + for (int index = 0; index < count; ++index) { + int status = 0; + const auto result = waitpid(children[index], &status, 0); + okay = result == children[index] && WIFEXITED(status) && WEXITSTATUS(status) == 0 && okay; + } + return okay; +} + +void Run() { + Object object; + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + if (unlinkObject == nullptr) throw std::runtime_error("guest shm_unlink export is missing"); + const int descriptor = shm_open_nid_postfix(object.name.c_str(), ReadWriteCreate, 0600); + if (descriptor < 0) throw std::runtime_error("shared object creation failed"); + + pid_t children[4]{}; + int childCount = 0; + bool okay = true; + for (int index = 0; index < 4; ++index) { + const pid_t child = fork(); + if (child < 0) { + okay = false; + break; + } + if (child == 0) { + const int own = (index & 1) ? shm_open_nid_postfix(object.name.c_str(), 2, 0600) : descriptor; + const bool childOkay = own >= 0 && Race(own); + if ((index & 1) && own >= 0) close_nid_postfix(own); + _exit(childOkay ? 0 : 1); + } + children[childCount++] = child; + } + okay = Race(descriptor) && okay; + okay = ChildrenSucceeded(children, childCount) && okay; + if (!okay) throw std::runtime_error("interleaved resize/write produced an invalid shared-object size"); + if (!NoDescriptorFallback(descriptor)) + throw std::runtime_error("EOF write with no spare descriptors grew the shared object or returned an unexpected error"); + + if (fchmod_nid_postfix(descriptor, 0400) != 0) + throw std::runtime_error("changing shared-object permissions to read-only failed"); + *__error_nid_postfix() = 0; + if (unlinkObject(object.name.c_str()) != -1 || *__error_nid_postfix() != 13) + throw std::runtime_error("unlink of a read-only shared object did not return EACCES"); + if (fchmod_nid_postfix(descriptor, 0600) != 0) + throw std::runtime_error("restoring shared-object permissions failed"); + if (unlinkObject(object.name.c_str()) != 0) + throw std::runtime_error("unlink after restoring write permission failed"); + if (close_nid_postfix(descriptor) != 0) + throw std::runtime_error("shared-object close failed"); + std::puts("cross-process truncate/write, descriptor sharing and unlink permissions passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmLifecycle.cpp b/core/libs/tests/GuestNamedShmLifecycle.cpp new file mode 100644 index 000000000..0d845cec7 --- /dev/null +++ b/core/libs/tests/GuestNamedShmLifecycle.cpp @@ -0,0 +1,211 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +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); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI dup2_nid_postfix(int, int); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int); +int APS5_VABI open_nid_postfix(const char*, int, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const struct GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const struct GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +struct GuestIovec { + void* base; + std::size_t length; +}; + +namespace { +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr int Private = 0x2; +constexpr std::size_t Page = 0x4000; +using Unlink = int (*)(const char*); + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-lifecycle-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; + +void Run() { + Object object; + const char* anonymousName = reinterpret_cast(1); + const int anonymous = shm_open_nid_postfix(anonymousName, 0x100e02, 0600); + Require(anonymous >= 0 && ftruncate_nid_postfix(anonymous, Page) == 0, "anonymous shared-memory setup failed"); + auto* anonymousMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, anonymous, 0)); + Require(anonymousMap != reinterpret_cast(static_cast(-1)), "anonymous MAP_SHARED failed"); + anonymousMap[0] = 'a'; + char anonymousByte = 0; + Require(pread_nid_postfix(anonymous, &anonymousByte, 1, 0) == 1 && anonymousByte == 'a', + "anonymous MAP_SHARED write was not visible through the descriptor"); + Require(munmap_nid_postfix(anonymousMap, Page) == 0 && close_nid_postfix(anonymous) == 0, "anonymous shared-memory cleanup failed"); + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0, "named object creation failed"); + Require(ftruncate_nid_postfix(descriptor, Page) == 0, "guest resize failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "old", 4, 0) == 4, "guest positioned write failed"); + char bytes[8]{}; + Require(pread_nid_postfix(descriptor, bytes, sizeof(bytes), Page) == 0, "read at EOF was not empty"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-length write failed"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, Page + 2, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "seek past EOF did not fail with EINVAL"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "seek to EOF returned the wrong position"); + Require(write_nid_postfix(descriptor, "z", 1) == 0, "write at EOF grew the shared object"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek near EOF failed"); + Require(write_nid_postfix(descriptor, "xy", 2) == 1, "cross-EOF write did not return its short length"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "cross-EOF write changed object size"); + char vectorBytes[] = "uv"; + GuestIovec vectorWrite{vectorBytes, 2}; + Require(pwritev_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-vector positioned write on a writable descriptor failed"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page) == 0, "positioned vector write at EOF grew the object"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page - 1) == 1, "positioned vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek before vector write failed"); + Require(writev_nid_postfix(descriptor, &vectorWrite, 1) == 1, "vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "vector write changed object size"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, -1, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "negative seek did not return EINVAL"); + Require(lseek_nid_postfix(descriptor, std::numeric_limits::max(), SEEK_CUR) == -1 && + *__error_nid_postfix() == 84, "overflowing seek did not return EOVERFLOW"); + + const int second = object.Open(ReadWrite); + Require(second >= 0 && pread_nid_postfix(second, bytes, 4, 0) == 4 && std::memcmp(bytes, "old", 4) == 0, + "independent open did not see the shared object"); + auto* firstMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + Require(firstMap != reinterpret_cast(static_cast(-1)), "guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(firstMap), Page), + "file-backed shared mapping was registered with write watch"); + auto* secondMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, second, 0)); + Require(secondMap != reinterpret_cast(static_cast(-1)), "second guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(secondMap), Page), + "file-backed shared alias was registered with write watch"); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, Page, 1, Shared, second, 1) == reinterpret_cast(static_cast(-1)) && + *__error_nid_postfix() == 22, "unaligned shared mapping offset did not return EINVAL"); + firstMap[0] = 'n'; + Require(secondMap[0] == 'n', "shared mappings did not observe each other's writes"); + auto* privateMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Private, second, 0)); + Require(privateMap != reinterpret_cast(static_cast(-1)), "guest private mapping failed"); + firstMap[1] = 'v'; + Require(privateMap[1] == 'v', "private mapping did not reflect backing changes before its first write"); + privateMap[0] = 'p'; + Require(firstMap[0] == 'n', "private mapping write escaped copy-on-write isolation"); + Require(pread_nid_postfix(second, bytes, 1, 0) == 1 && bytes[0] == 'n', "private mapping write changed the backing object"); + const int readOnly = object.Open(ReadOnly); + Require(readOnly >= 0, "read-only open failed"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, 0) == -1 && *__error_nid_postfix() == 9, + "positioned write on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, Page) == -1 && *__error_nid_postfix() == 9, + "positioned write at EOF on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(readOnly, nullptr, 0, 0) == -1 && *__error_nid_postfix() == 9, + "zero-vector positioned write on a read-only descriptor did not return EBADF"); + FileStat readOnlyInfo{}; + Require(fstat_nid_disambig1_nid_postfix(readOnly, &readOnlyInfo) == 0 && readOnlyInfo.st_size == Page, + "read-only EOF write changed the shared-object size"); + void* rejected = mmap_nid_postfix(nullptr, Page, 3, Shared, readOnly, 0); + Require(rejected == reinterpret_cast(static_cast(-1)), "writable shared mapping accepted a read-only descriptor"); + auto* writableMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, descriptor, 0)); + Require(writableMap != reinterpret_cast(static_cast(-1)), "read-protected writable-fd mapping failed"); + Require(mprotect_nid_postfix(writableMap, Page, 3) == 0, "mprotect could not restore write access from a writable descriptor"); + writableMap[2] = 'm'; + Require(firstMap[2] == 'm', "mprotect-upgraded mapping did not remain shared"); + Require(munmap_nid_postfix(writableMap, Page) == 0, "upgraded mapping unmap failed"); + auto* readOnlyMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, readOnly, 0)); + Require(readOnlyMap != reinterpret_cast(static_cast(-1)), "read-only shared mapping failed"); + *__error_nid_postfix() = 0; + Require(mprotect_nid_postfix(readOnlyMap, Page, 3) == -1 && *__error_nid_postfix() == 13, + "mprotect on a read-only shared mapping did not return EACCES"); + Require(munmap_nid_postfix(readOnlyMap, Page) == 0, "read-only mapping unmap failed"); + + const int ordinary = ::open(__FILE__, O_RDONLY); + Require(ordinary >= 0, "ordinary file setup failed"); + Require(dup2_nid_postfix(ordinary, second) == second, "dup2 onto shared descriptor failed"); + Require(lseek_nid_postfix(second, 0, SEEK_SET) == 0 && read_nid_postfix(second, bytes, sizeof(bytes)) > 0, + "dup2 target did not become the ordinary file"); + Require(close_nid_postfix(second) == 0, "close after dup2 failed"); + Require(lseek_nid_postfix(ordinary, 0, SEEK_SET) == 0 && read_nid_postfix(ordinary, bytes, sizeof(bytes)) > 0, + "closing reused descriptor damaged the ordinary file"); + Require(close_nid_postfix(ordinary) == 0, "ordinary file close failed"); + const int reused = object.Open(ReadWrite); + Require(reused >= 0, "shared object reopen after descriptor close failed"); + Require(dup2_nid_postfix(reused, second) == second, "guest dup2 into the freed descriptor slot failed"); + Require(pread_nid_postfix(second, bytes, 3, 0) == 3 && std::memcmp(bytes, "nvm", 3) == 0, + "reused descriptor slot retained ordinary-file state"); + Require(close_nid_postfix(second) == 0, "reused descriptor slot close failed"); + if (reused != second) Require(close_nid_postfix(reused) == 0, "reopened descriptor close failed"); + + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(readOnly) == 0, "descriptor close failed"); + Require(firstMap[0] == 'n', "mapping did not survive descriptor close"); + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + Require(unlinkObject != nullptr, "guest shm_unlink export is missing"); + Require(unlinkObject(object.name.c_str()) == 0, "guest unlink failed"); + *__error_nid_postfix() = 0; + Require(unlinkObject(object.name.c_str()) == -1 && *__error_nid_postfix() == 2, "missing-name unlink did not return ENOENT"); + const int recreated = object.Open(ReadWrite | Create | Exclusive); + Require(recreated >= 0, "name recreation after unlink failed"); + Require(ftruncate_nid_postfix(recreated, Page) == 0, "recreated object resize failed"); + Require(pread_nid_postfix(recreated, bytes, 4, 0) == 4 && std::memcmp(bytes, "\0\0\0\0", 4) == 0, + "recreated name still refers to the unlinked object"); + Require(firstMap[0] == 'n', "unlink or recreation changed the old mapping"); + Require(close_nid_postfix(recreated) == 0, "recreated descriptor close failed"); + Require(munmap_nid_postfix(firstMap, Page) == 0 && munmap_nid_postfix(secondMap, Page) == 0 && munmap_nid_postfix(privateMap, Page) == 0, + "guest unmap failed"); + std::puts("named shared-memory lifecycle checks passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp index 357f6445a..c4f3c16f4 100644 --- a/core/libs/tests/GuestShmOpen.cpp +++ b/core/libs/tests/GuestShmOpen.cpp @@ -37,6 +37,10 @@ int main() { Require(pread_nid_postfix(first, &last, 1, 0x3fff) == 1 && last == 0); Require(pread_nid_postfix(first, &last, 1, 0x4000) == 0); Require(pread_nid_postfix(second, back, sizeof(back), 0) == 0); +#ifndef _WIN32 + Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == 0); +#endif + Require(ftruncate_nid_postfix(third, 0x4000) == 0); Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == static_cast(sizeof(text))); Require(close_nid_postfix(first) == 0 && close_nid_postfix(second) == 0 && close_nid_postfix(third) == 0); RequireInvalid(anonymous, 0x0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8ada18bc4..60aaab7ea 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -1,6 +1,5 @@ # Project technical debt -- Linux [shm_open](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) maps slash-prefixed guest names of up to 1023 bytes to `/anyps5-shm-` plus the existing SHA-1 digest. The namespace is shared across AnyPS5 processes in the host mount namespace; digest collisions are not detected. Named objects persist until host unlink or reboot because guest `shm_unlink` is absent. Named objects on Windows still throw. File-backed guest `mmap` is unsupported; descriptor sharing is tested with native Linux mappings. Guest invalid pointers other than null cannot be reported safely as `EFAULT`. Console behaviour and title compatibility remain unverified. ### Build @@ -397,7 +396,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - 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. - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. 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. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. -- [shm_open](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) creates only anonymous objects (`SHM_ANON`), each backed by a host anonymous file: a `memfd` on Linux, a delete-on-close temporary file on Windows. The flag checks follow FreeBSD 11's `kern_shm_open`; the mode is ignored. Named objects throw. The descriptor cannot be mapped yet: `mmap` accepts only private anonymous mappings and fails a descriptor mapping with `EOPNOTSUPP`, so the known caller (the bundled Mesa in PPSA59322, which truncates the object and maps it `MAP_SHARED`) gets that failure +- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the 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 - [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 - [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 From 72b3a2abdd96026f0fe87ae2f2b95b17c2dc7492 Mon Sep 17 00:00:00 2001 From: bilel Date: Sun, 11 Oct 2026 14:28:21 +0100 Subject: [PATCH 247/277] fix(libkernel): keep shared-memory identity and guest permissions consistent Pin the descriptor before mmap validation so guest descriptor reuse cannot redirect the eventual mapping. Apply the canonical guest creation mask when creating named objects, distinguishing new objects before setting permissions without changing the host process umask. Add regressions that reproduce both failures and document the remaining host requirements. Constraint: Guest umask must not mutate the process-wide host mask Confidence: high Scope-risk: narrow Tested: 17 targeted Linux runtime tests passed with zero skips; conventions and diff checks; Clang analyzer on three changed production translation units Not-tested: Full project build and suite, Windows runtime, console hardware and titles --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../libkernel/DirectMemory/PosixMemory.cpp | 31 +++- .../libkernel/File/include/SharedMemory.hpp | 1 + .../prx/libkernel/File/src/SharedMemory.cpp | 29 +++- core/libs/prx/libkernel_sys/Export.cpp | 6 +- .../tests/GuestNamedShmDescriptorReuse.cpp | 160 ++++++++++++++++++ core/libs/tests/GuestNamedShmOpen.cpp | 2 +- core/libs/tests/GuestNamedShmPermissions.cpp | 83 +++++++++ docs/dev/TechnicalDebt.md | 4 +- 9 files changed, 312 insertions(+), 14 deletions(-) create mode 100644 core/libs/tests/GuestNamedShmDescriptorReuse.cpp create mode 100644 core/libs/tests/GuestNamedShmPermissions.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8bd7c4c86..3fadd83f5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -766,6 +766,16 @@ if(BUILD_TESTING) target_link_libraries(guest_named_shm_lifecycle_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) add_test(NAME guest_named_shm_lifecycle COMMAND guest_named_shm_lifecycle_tests) set_tests_properties(guest_named_shm_lifecycle PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmDescriptorReuse.cpp) + target_include_directories(guest_named_shm_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_descriptor_reuse_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_descriptor_reuse COMMAND guest_named_shm_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_descriptor_reuse PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_permissions_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmPermissions.cpp) + target_include_directories(guest_named_shm_permissions_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_permissions_tests PRIVATE libkernel_sys libkernel libc rt) + add_test(NAME guest_named_shm_permissions COMMAND guest_named_shm_permissions_tests) + set_tests_properties(guest_named_shm_permissions PROPERTIES TIMEOUT 20) add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp) target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 94bdad8af..479203763 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -40,6 +40,25 @@ constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; constexpr int GuestAlignedSuper = 0x1000000; constexpr std::size_t SuperpageSize = std::size_t{1} << 21; +#if defined(__linux__) +class ScopedDescriptor { +public: + explicit ScopedDescriptor(int value) noexcept : descriptor(fcntl(value, F_DUPFD_CLOEXEC, 0)), error(descriptor < 0 ? errno : 0) {} + ~ScopedDescriptor() { + const int savedError = errno; + if (descriptor >= 0) close(descriptor); + errno = savedError; + } + ScopedDescriptor(const ScopedDescriptor&) = delete; + ScopedDescriptor& operator=(const ScopedDescriptor&) = delete; + int Get() const noexcept { return descriptor; } + int Error() const noexcept { return error; } +private: + int descriptor = -1; + int error = 0; +}; +#endif + bool RoundLength(std::size_t length, std::size_t& rounded) { constexpr auto mask = PS5_PAGE_SIZE - 1; if (length == 0 || length > std::numeric_limits::max() - mask) @@ -95,18 +114,24 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti static_cast(offset) > static_cast(std::numeric_limits::max()) - rounded) return failed(GuestInvalid); #if defined(__linux__) - const int descriptorFlags = fcntl(descriptor, F_GETFL); + const ScopedDescriptor pinnedDescriptor(descriptor); + if (pinnedDescriptor.Get() < 0) return failed(pinnedDescriptor.Error()); + const int mappingDescriptor = pinnedDescriptor.Get(); + const int descriptorFlags = fcntl(mappingDescriptor, F_GETFL); if (descriptorFlags < 0) return failed(errno); const int accessMode = descriptorFlags & O_ACCMODE; if ((protection & 1) != 0 && accessMode == O_WRONLY) return failed(GuestAccess); if (kind == GuestShared && (protection & 2) != 0 && accessMode == O_RDONLY) return failed(GuestAccess); + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); +#else + const int mappingDescriptor = descriptor; + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); #endif - if (!File::IsSharedMemoryDescriptor_nid_no_patch(descriptor)) return failed(GuestNotSupported); const std::size_t alignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; void* mapped = nullptr; try { mapped = address; - const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, descriptor, static_cast(offset), alignment); + const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, mappingDescriptor, static_cast(offset), alignment); if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); return mapped; } catch (const std::invalid_argument&) { diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp index 5c9ffc9f4..96e293c33 100644 --- a/core/libs/prx/libkernel/File/include/SharedMemory.hpp +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -9,6 +9,7 @@ namespace File { +std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask); bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 25d157204..3e685679b 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -12,14 +12,13 @@ #include #include #include -#include +#include #ifdef _WIN32 #include #include #include #include -#include #include #include #else @@ -80,6 +79,8 @@ private: int error = 0; }; +std::atomic creationMask{022}; + constexpr int GuestNoMemory = 12; constexpr int GuestInvalid = 22; constexpr int GuestTooManyFilesInSystem = 23; @@ -163,9 +164,25 @@ int OpenNamedFile(const char* path, int flags, int mode) { if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; - const int descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, mode & 0777); + const auto permissions = (mode & 0777) & ~creationMask.load(std::memory_order_relaxed); + bool created = false; + int descriptor; + if (flags & SCE_KERNEL_O_CREAT) { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC | O_EXCL, permissions); + created = descriptor >= 0; + if (descriptor < 0 && errno == EEXIST && !(flags & SCE_KERNEL_O_EXCL)) { + descriptor = ::shm_open(hostName.c_str(), (hostFlags & ~(O_CREAT | O_EXCL)) | O_CLOEXEC, 0); + } + } else { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, 0); + } if (descriptor >= 0) { - return descriptor; + if (!created || ::fchmod(descriptor, permissions) == 0) return descriptor; + const int error = errno; + ::close(descriptor); + ::shm_unlink(hostName.c_str()); + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: setting guest creation permissions failed, errno=" + std::to_string(error)); } const int error = errno; if (const int guest = NamedGuestError(error)) return Fail(guest); @@ -202,6 +219,10 @@ int CreateAnonymousFile() { namespace File { +std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) { + return creationMask.exchange(static_cast(mask & 07777), std::memory_order_relaxed); +} + bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { return IsHostSharedMemoryDescriptor(descriptor); } diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 367fefd37..0d408ffaf 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -17,6 +16,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -38,8 +38,6 @@ constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; -std::atomic creationMask{022}; - std::int64_t Fail(int error) { *__error_nid_postfix() = error; return -1; @@ -63,7 +61,7 @@ int APS5_VABI setsid_nid_postfix(void) { } std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { - return creationMask.exchange(static_cast(mask & 07777)); + return File::ExchangeCreationMask_nid_no_patch(mask); } int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); diff --git a/core/libs/tests/GuestNamedShmDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp new file mode 100644 index 000000000..aa6609da9 --- /dev/null +++ b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp @@ -0,0 +1,160 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI ftruncate_nid_postfix(int, 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); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr std::size_t Page = 0x4000; +int armedDescriptor = -1; +int replacementDescriptor = -1; +bool armed = false; +bool swapped = false; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-descriptor-reuse-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; +} + +extern "C" int fcntl(int descriptor, int command, ...) { + long argument = 0; + switch (command) { + case F_GETFD: + case F_GETFL: + case F_GETOWN: +#ifdef F_GETSIG + case F_GETSIG: +#endif +#ifdef F_GETLEASE + case F_GETLEASE: +#endif +#ifdef F_GETPIPE_SZ + case F_GETPIPE_SZ: +#endif +#ifdef F_GET_SEALS + case F_GET_SEALS: +#endif + break; +#ifdef F_GETLK + case F_GETLK: + case F_SETLK: + case F_SETLKW: { + va_list args; + va_start(args, command); + argument = reinterpret_cast(va_arg(args, void*)); + va_end(args); + break; + } +#endif + case F_DUPFD: + case F_DUPFD_CLOEXEC: + case F_SETFD: + case F_SETFL: + case F_SETOWN: +#ifdef F_SETSIG + case F_SETSIG: +#endif +#ifdef F_SETLEASE + case F_SETLEASE: +#endif +#ifdef F_NOTIFY + case F_NOTIFY: +#endif +#ifdef F_SETPIPE_SZ + case F_SETPIPE_SZ: +#endif +#ifdef F_ADD_SEALS + case F_ADD_SEALS: +#endif + { + va_list args; + va_start(args, command); + argument = va_arg(args, int); + va_end(args); + break; + } + default: + errno = EINVAL; + return -1; + } + const long result = ::syscall(SYS_fcntl, descriptor, command, argument); + if (command == F_GETFL && armed && !swapped) { + swapped = true; + if (::syscall(SYS_dup2, replacementDescriptor, armedDescriptor) != armedDescriptor) return -1; + } + return static_cast(result); +} + +int main() { + try { + Object object; + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, Page) == 0, "shared-memory setup failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "original", 8, 0) == 8, "shared-memory seed failed"); + const int verifier = object.Open(ReadWrite); + Require(verifier >= 0, "shared-memory verifier open failed"); + const int replacement = ::open(__FILE__, O_RDONLY); + Require(replacement >= 0, "ordinary-file setup failed"); + armedDescriptor = descriptor; + replacementDescriptor = replacement; + armed = true; + auto* mapped = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + armed = false; + Require(swapped, "native F_GETFL interposer did not replace the guest descriptor"); + Require(mapped != reinterpret_cast(static_cast(-1)), "guest mmap failed after descriptor replacement"); + Require(std::memcmp(mapped, "original", 8) == 0, "mapping followed the replacement descriptor instead of the pinned shared-memory object"); + mapped[0] = 0x70; + char byte = 0; + Require(pread_nid_postfix(verifier, &byte, 1, 0) == 1 && byte == 0x70, "mapping did not update the original shared-memory object"); + Require(munmap_nid_postfix(mapped, Page) == 0, "guest mapping cleanup failed"); + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(verifier) == 0, "guest descriptor cleanup failed"); + Require(::close(replacement) == 0, "replacement descriptor cleanup failed"); + std::puts("named shared-memory descriptor reuse checks passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmOpen.cpp b/core/libs/tests/GuestNamedShmOpen.cpp index ecc39d164..dc6cebf44 100644 --- a/core/libs/tests/GuestNamedShmOpen.cpp +++ b/core/libs/tests/GuestNamedShmOpen.cpp @@ -99,7 +99,7 @@ static void Run() { const int longDescriptor = longName.Open(create | readWrite, 07777); umask(previousMask); Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed"); - Require((info.st_mode & 07777) == 0700, "mode or umask was not applied"); + Require((info.st_mode & 07777) == 0755, "default guest umask was not applied independently of host umask"); Require(close(longDescriptor) == 0, "long name close failed"); const int longReader = longName.Open(0); Require(longReader >= 0 && close(longReader) == 0, "maximum-length name reopen failed"); diff --git a/core/libs/tests/GuestNamedShmPermissions.cpp b/core/libs/tests/GuestNamedShmPermissions.cpp new file mode 100644 index 000000000..c6f8d4c1b --- /dev/null +++ b/core/libs/tests/GuestNamedShmPermissions.cpp @@ -0,0 +1,83 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Masks { +public: + Masks(std::uint16_t guest, mode_t host) : guest(umask_nid_postfix(guest)), host(::umask(host)) {} + ~Masks() { ::umask(host); umask_nid_postfix(guest); } + Masks(const Masks&) = delete; + Masks& operator=(const Masks&) = delete; +private: + std::uint16_t guest; + mode_t host; +}; + +class Object { +public: + explicit Object(const char* suffix) : name("/anyps5-permissions-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + int descriptor = -1; +private: + std::string hostName = "/anyps5-shm-"; +}; + +static void CheckMode(const char* suffix, std::uint16_t guestMask, mode_t hostMask, int requested, mode_t expected) { + Masks masks(guestMask, hostMask); + Object object(suffix); + object.descriptor = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200 | 0x800, requested); + Require(object.descriptor >= 0, "shared-memory creation failed"); + struct stat info{}; + Require(::fstat(object.descriptor, &info) == 0, "stat failed"); + if ((info.st_mode & 07777) != expected) { + std::fprintf(stderr, "requested=%o guest-mask=%o host-mask=%o actual=%o expected=%o\n", requested, guestMask, hostMask, info.st_mode & 07777, expected); + throw std::runtime_error("shared-memory mode did not follow the guest umask"); + } + const auto observedHostMask = ::umask(hostMask); + Require(observedHostMask == hostMask, "guest creation changed host umask"); + if (expected == 0600) { + const int reopened = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200, 0777); + Require(reopened >= 0, "existing-object reopen failed"); + const int result = ::fstat(reopened, &info); + ::close(reopened); + Require(result == 0 && (info.st_mode & 07777) == expected, "reopen changed existing permissions"); + } +} + +int main() { + try { + CheckMode("-guest-restricts", 0077, 0000, 0666, 0600); + CheckMode("-host-restricts", 0000, 0077, 0666, 0666); + CheckMode("-guest-denies", 0777, 0000, 0777, 0000); + CheckMode("-special-bits", 0022, 0077, 07777, 0755); + std::puts("guest shared-memory permissions passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 60aaab7ea..7b714f522 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -396,7 +396,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - 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. - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. 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. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. -- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. +- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the 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 - [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 - [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 @@ -431,7 +431,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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. -- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, but files are still created with the host process's mask +- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. - [2D stores to 1D storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). From f6ef5a77e289cfc800c77296d2819bc52f808c82 Mon Sep 17 00:00:00 2001 From: bilel Date: Sun, 11 Oct 2026 14:50:10 +0100 Subject: [PATCH 248/277] fix(libkernel): preserve patched loading and shared-memory object identity Keep guest creation-mask state in libc behind C-linkage helpers so NID patching preserves the shared exports. Reject a pinned descriptor whose file identity differs from the snapshot taken before classification. Exercise close and reuse from another thread at both acquisition windows for writes, resize and seek, and create temporary ordinary-file fixtures so runtime tests work without the source tree. Constraint: NID exemptions require the literal suffix at the end of the exported symbol Confidence: high Scope-risk: narrow Tested: 18 targeted Linux runtime tests with zero skips; patched_libraries for libc/libkernel/libkernel_sys; standalone ASan/UBSan with warnings as errors; source-prefix-remapped fixtures; Clang analyzer and conventions Not-tested: Full project build and library set, Windows runtime, console hardware and titles Directive: Device/inode validation establishes file identity, not open-file-description identity --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/include/CreationMask.hpp | 13 ++ core/libs/prx/libc/src/CreationMask.cpp | 21 +++ core/libs/prx/libkernel/CMakeLists.txt | 5 + .../libkernel/File/include/SharedMemory.hpp | 1 - .../prx/libkernel/File/src/SharedMemory.cpp | 18 +-- core/libs/prx/libkernel_sys/Export.cpp | 2 +- .../tests/GuestNamedShmDescriptorReuse.cpp | 19 ++- .../tests/GuestNamedShmIoDescriptorReuse.cpp | 123 ++++++++++++++++++ core/libs/tests/GuestNamedShmLifecycle.cpp | 19 ++- core/libs/tests/shared_memory/CMakeLists.txt | 1 + docs/dev/TechnicalDebt.md | 2 +- 12 files changed, 212 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libc/include/CreationMask.hpp create mode 100644 core/libs/prx/libc/src/CreationMask.cpp create mode 100644 core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ddfe85664..66f640339 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp diff --git a/core/libs/prx/libc/include/CreationMask.hpp b/core/libs/prx/libc/include/CreationMask.hpp new file mode 100644 index 000000000..d6d419b02 --- /dev/null +++ b/core/libs/prx/libc/include/CreationMask.hpp @@ -0,0 +1,13 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP + +#include + +namespace File { + +extern "C" std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask); +extern "C" std::uint16_t GetCreationMask_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libc/src/CreationMask.cpp b/core/libs/prx/libc/src/CreationMask.cpp new file mode 100644 index 000000000..a4491eccd --- /dev/null +++ b/core/libs/prx/libc/src/CreationMask.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/CreationMask.hpp" + +#include + +namespace { + +std::atomic creationMask{022}; + +} + +namespace File { + +std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) { + return creationMask.exchange(static_cast(mask & 07777), std::memory_order_relaxed); +} + +std::uint16_t GetCreationMask_nid_no_patch() { + return creationMask.load(std::memory_order_relaxed); +} + +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3fadd83f5..9d21c7bf2 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -776,6 +776,11 @@ if(BUILD_TESTING) target_link_libraries(guest_named_shm_permissions_tests PRIVATE libkernel_sys libkernel libc rt) add_test(NAME guest_named_shm_permissions COMMAND guest_named_shm_permissions_tests) set_tests_properties(guest_named_shm_permissions PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_io_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmIoDescriptorReuse.cpp) + target_include_directories(guest_named_shm_io_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_io_descriptor_reuse_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_io_descriptor_reuse COMMAND guest_named_shm_io_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_io_descriptor_reuse PROPERTIES TIMEOUT 20) add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp) target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp index 96e293c33..5c9ffc9f4 100644 --- a/core/libs/prx/libkernel/File/include/SharedMemory.hpp +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -9,7 +9,6 @@ namespace File { -std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask); bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 3e685679b..92db94e0f 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/File.hpp" #include @@ -79,8 +80,6 @@ private: int error = 0; }; -std::atomic creationMask{022}; - constexpr int GuestNoMemory = 12; constexpr int GuestInvalid = 22; constexpr int GuestTooManyFilesInSystem = 23; @@ -164,7 +163,7 @@ int OpenNamedFile(const char* path, int flags, int mode) { if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; - const auto permissions = (mode & 0777) & ~creationMask.load(std::memory_order_relaxed); + const auto permissions = (mode & 0777) & ~File::GetCreationMask_nid_no_patch(); bool created = false; int descriptor; if (flags & SCE_KERNEL_O_CREAT) { @@ -219,10 +218,6 @@ int CreateAnonymousFile() { namespace File { -std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) { - return creationMask.exchange(static_cast(mask & 07777), std::memory_order_relaxed); -} - bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { return IsHostSharedMemoryDescriptor(descriptor); } @@ -232,6 +227,10 @@ namespace { class SharedMemoryLock { public: explicit SharedMemoryLock(int value) { +#ifdef __linux__ + struct stat originalStatus{}; + if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } +#endif if (!IsHostSharedMemoryDescriptor(value)) return; shared = true; hostLock.emplace(); @@ -239,7 +238,10 @@ public: descriptor = Duplicate(value); if (descriptor < 0) { error = errno; return; } if (!IsHostSharedMemoryDescriptor(descriptor)) { error = EBADF; return; } - if (!Refresh()) error = errno; + if (!Refresh()) { error = errno; return; } +#ifdef __linux__ + if (status.st_dev != originalStatus.st_dev || status.st_ino != originalStatus.st_ino) error = EBADF; +#endif } ~SharedMemoryLock() { diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 0d408ffaf..49ad57d31 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -16,7 +16,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" -#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libc/include/CreationMask.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX diff --git a/core/libs/tests/GuestNamedShmDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp index aa6609da9..e2b7263e2 100644 --- a/core/libs/tests/GuestNamedShmDescriptorReuse.cpp +++ b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp @@ -37,6 +37,23 @@ int replacementDescriptor = -1; bool armed = false; bool swapped = false; +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-descriptor-reuse-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); } @@ -135,7 +152,7 @@ int main() { Require(pwrite_nid_disambig1_nid_postfix(descriptor, "original", 8, 0) == 8, "shared-memory seed failed"); const int verifier = object.Open(ReadWrite); Require(verifier >= 0, "shared-memory verifier open failed"); - const int replacement = ::open(__FILE__, O_RDONLY); + const int replacement = CreateOrdinaryFile(); Require(replacement >= 0, "ordinary-file setup failed"); armedDescriptor = descriptor; replacementDescriptor = replacement; diff --git a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp new file mode 100644 index 000000000..bd6af4b58 --- /dev/null +++ b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp @@ -0,0 +1,123 @@ +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { void* base; std::size_t length; }; +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} +namespace { +int original = -1; +int replacement = -1; +bool armed = false; +bool replaceAtProbe = false; +bool swapped = false; +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +struct Object { + explicit Object(const char* suffix) : name("/anyps5-io-reuse-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + descriptor = shm_open_nid_postfix(name.c_str(), 2 | 0x200 | 0x800, 0600); + Require(descriptor >= 0, "object creation failed"); + Require(::ftruncate(descriptor, 0x4000) == 0, "object sizing failed"); + Require(::pwrite(descriptor, "safe", 4, 0) == 4, "object seeding failed"); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + std::string hostName = "/anyps5-shm-"; + int descriptor = -1; +}; +void Run(int operation, bool atProbe) { + replaceAtProbe = atProbe; + Object first(("-first-" + std::to_string(operation)).c_str()); + Object second(("-second-" + std::to_string(operation)).c_str()); + original = first.descriptor; + replacement = second.descriptor; + swapped = false; + armed = true; + char payload[] = "bad!"; + GuestIovec vector{payload, 4}; + std::int64_t result = 0; + switch (operation) { + case 0: result = write_nid_postfix(original, payload, 4); break; + case 1: result = pwrite_nid_disambig1_nid_postfix(original, payload, 4, 0); break; + case 2: result = ftruncate_nid_postfix(original, 7); break; + case 3: result = lseek_nid_postfix(original, 7, SEEK_SET); break; + case 4: result = writev_nid_postfix(original, &vector, 1); break; + case 5: result = pwritev_nid_postfix(original, &vector, 1, 0); break; + } + armed = false; + Require(swapped, "descriptor replacement did not run at the namespace-lock boundary"); + struct stat status{}; + char bytes[4]{}; + Require(::fstat(replacement, &status) == 0 && status.st_size == 0x4000, "replacement object size changed"); + Require(::pread(replacement, bytes, 4, 0) == 4 && std::memcmp(bytes, "safe", 4) == 0, "replacement object data changed"); + Require(::lseek(replacement, 0, SEEK_CUR) == 0, "replacement object offset changed"); + Require(result == -1 && *__error_nid_postfix() == 9, "descriptor identity change was not rejected with EBADF"); +} +} +static bool ReplaceDescriptor() { + swapped = true; + bool replaced = false; + std::thread reuse([&] { + replaced = close_nid_postfix(original) == 0 && dup2_nid_postfix(replacement, original) == original; + }); + reuse.join(); + return replaced; +} +extern "C" ssize_t readlink(const char* path, char* buffer, std::size_t length) { + const auto result = static_cast(::syscall(SYS_readlink, path, buffer, length)); + if (result >= 0 && armed && replaceAtProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +extern "C" int flock(int descriptor, int operation) { + const int result = static_cast(::syscall(SYS_flock, descriptor, operation)); + if (result == 0 && operation == LOCK_EX && armed && !replaceAtProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +int main() { + int failures = 0; + for (int scenario = 0; scenario < 12; ++scenario) { + const int operation = scenario % 6; + try { + Run(operation, scenario >= 6); + } catch (const std::exception& error) { + std::fprintf(stderr, "scenario %d: %s\n", scenario, error.what()); + ++failures; + } + } + if (failures != 0) return 1; + std::puts("guest shared-memory I/O descriptor reuse passed"); + return 0; +} diff --git a/core/libs/tests/GuestNamedShmLifecycle.cpp b/core/libs/tests/GuestNamedShmLifecycle.cpp index 0d845cec7..a5c26b8be 100644 --- a/core/libs/tests/GuestNamedShmLifecycle.cpp +++ b/core/libs/tests/GuestNamedShmLifecycle.cpp @@ -50,6 +50,23 @@ constexpr int Private = 0x2; constexpr std::size_t Page = 0x4000; using Unlink = int (*)(const char*); +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-lifecycle-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); } @@ -163,7 +180,7 @@ void Run() { "mprotect on a read-only shared mapping did not return EACCES"); Require(munmap_nid_postfix(readOnlyMap, Page) == 0, "read-only mapping unmap failed"); - const int ordinary = ::open(__FILE__, O_RDONLY); + const int ordinary = CreateOrdinaryFile(); Require(ordinary >= 0, "ordinary file setup failed"); Require(dup2_nid_postfix(ordinary, second) == second, "dup2 onto shared descriptor failed"); Require(lseek_nid_postfix(second, 0, SEEK_SET) == 0 && read_nid_postfix(second, bytes, sizeof(bytes)) > 0, diff --git a/core/libs/tests/shared_memory/CMakeLists.txt b/core/libs/tests/shared_memory/CMakeLists.txt index 6fe63c09b..f9e85f8de 100644 --- a/core/libs/tests/shared_memory/CMakeLists.txt +++ b/core/libs/tests/shared_memory/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(guest_named_shm_open_tests ../GuestNamedShmOpen.cpp Support.cpp ${LIBS_DIR}/prx/libkernel/File/src/SharedMemory.cpp + ${LIBS_DIR}/prx/libc/src/CreationMask.cpp ${LIBS_DIR}/nid/src/Sha1.cpp) target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_DIR} ${LIBS_DIR}/nid/include) target_compile_features(guest_named_shm_open_tests PRIVATE cxx_std_20) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7b714f522..9061ca9a5 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -396,7 +396,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - 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. - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. 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. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. -- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. +- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and reject a pinned object whose device/inode differs from the initial descriptor identity. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the 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 - [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 - [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 From 11f329fd4b1c60406d6429e3b1972078726a45e2 Mon Sep 17 00:00:00 2001 From: bilel Date: Sun, 11 Oct 2026 14:59:28 +0100 Subject: [PATCH 249/277] fix(libkernel): reject identity changes before ordinary-file fallback A descriptor replaced before the shared-memory path probe can otherwise bypass the pinned-object identity check through native fallback. Compare the negative probe's current file identity to the initial snapshot and reject a mismatch. Exercise the earlier window with an ordinary file for all six descriptor operations. Confidence: high Scope-risk: narrow Tested: 18 Linux runtime tests with zero skips; 18 deterministic reuse scenarios; affected three patched modules; standalone ASan/UBSan; Clang analyzer; conventions and diff checks Not-tested: Full project build/suite, Windows runtime, console hardware and titles Directive: Identity checks do not synchronize the entire descriptor table or identify separate opens of the same inode --- .../prx/libkernel/File/src/SharedMemory.cpp | 12 +++++++- .../tests/GuestNamedShmIoDescriptorReuse.cpp | 28 +++++++++++++++---- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 92db94e0f..8202db42e 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -231,7 +231,17 @@ public: struct stat originalStatus{}; if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } #endif - if (!IsHostSharedMemoryDescriptor(value)) return; + if (!IsHostSharedMemoryDescriptor(value)) { +#ifdef __linux__ + struct stat currentStatus{}; + if (::fstat(value, ¤tStatus) != 0) { shared = true; error = errno; return; } + if (currentStatus.st_dev != originalStatus.st_dev || currentStatus.st_ino != originalStatus.st_ino) { + shared = true; + error = EBADF; + } +#endif + return; + } shared = true; hostLock.emplace(); if (hostLock->Error() != 0) { error = hostLock->Error(); return; } diff --git a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp index bd6af4b58..368fb3cc6 100644 --- a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp +++ b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -31,6 +32,7 @@ int original = -1; int replacement = -1; bool armed = false; bool replaceAtProbe = false; +bool replaceBeforeProbe = false; bool swapped = false; void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -54,10 +56,21 @@ struct Object { std::string hostName = "/anyps5-shm-"; int descriptor = -1; }; -void Run(int operation, bool atProbe) { - replaceAtProbe = atProbe; +void Run(int operation, int boundary) { + replaceAtProbe = boundary == 1; + replaceBeforeProbe = boundary == 2; Object first(("-first-" + std::to_string(operation)).c_str()); Object second(("-second-" + std::to_string(operation)).c_str()); + if (replaceBeforeProbe) { + char path[] = "/tmp/anyps5-io-reuse-XXXXXX"; + const int temporary = ::mkstemp(path); + Require(temporary >= 0, "ordinary replacement creation failed"); + Require(::unlink(path) == 0, "ordinary replacement unlink failed"); + Require(::ftruncate(temporary, 0x4000) == 0 && ::pwrite(temporary, "safe", 4, 0) == 4, + "ordinary replacement initialization failed"); + Require(::dup2(temporary, second.descriptor) == second.descriptor, "ordinary replacement installation failed"); + ::close(temporary); + } original = first.descriptor; replacement = second.descriptor; swapped = false; @@ -74,7 +87,7 @@ void Run(int operation, bool atProbe) { case 5: result = pwritev_nid_postfix(original, &vector, 1, 0); break; } armed = false; - Require(swapped, "descriptor replacement did not run at the namespace-lock boundary"); + Require(swapped, "descriptor replacement did not run at the selected acquisition boundary"); struct stat status{}; char bytes[4]{}; Require(::fstat(replacement, &status) == 0 && status.st_size == 0x4000, "replacement object size changed"); @@ -93,6 +106,9 @@ static bool ReplaceDescriptor() { return replaced; } extern "C" ssize_t readlink(const char* path, char* buffer, std::size_t length) { + if (armed && replaceBeforeProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } const auto result = static_cast(::syscall(SYS_readlink, path, buffer, length)); if (result >= 0 && armed && replaceAtProbe && !swapped) { if (!ReplaceDescriptor()) return -1; @@ -101,17 +117,17 @@ extern "C" ssize_t readlink(const char* path, char* buffer, std::size_t length) } extern "C" int flock(int descriptor, int operation) { const int result = static_cast(::syscall(SYS_flock, descriptor, operation)); - if (result == 0 && operation == LOCK_EX && armed && !replaceAtProbe && !swapped) { + if (result == 0 && operation == LOCK_EX && armed && !replaceAtProbe && !replaceBeforeProbe && !swapped) { if (!ReplaceDescriptor()) return -1; } return result; } int main() { int failures = 0; - for (int scenario = 0; scenario < 12; ++scenario) { + for (int scenario = 0; scenario < 18; ++scenario) { const int operation = scenario % 6; try { - Run(operation, scenario >= 6); + Run(operation, scenario / 6); } catch (const std::exception& error) { std::fprintf(stderr, "scenario %d: %s\n", scenario, error.what()); ++failures; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9061ca9a5..4c52a9fce 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -396,7 +396,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - 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. - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. 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. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. -- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and reject a pinned object whose device/inode differs from the initial descriptor identity. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. +- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the 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 - [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 - [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 From 9ac63c0af823d8be0501f7710360b5771efd6a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:40:51 +0200 Subject: [PATCH 250/277] style: restore blank lines touched by accident --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 1 + docs/dev/TechnicalDebt.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index afa6d4420..9eeb24861 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -673,6 +673,7 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { std::mutex g_reservationLock; std::map g_reservations; + void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast(addr); const auto end = start + len; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4c52a9fce..820357326 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -1,6 +1,5 @@ # Project technical debt - ### Build - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) From ebf361103c2178186dd01203f65b6c7c53ad4071 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sat, 3 Oct 2026 01:06:40 +0200 Subject: [PATCH 251/277] feat(agc): multisampled color and depth targets with CB resolves Draws with PA_SC_AA_CONFIG multisampling (2, 4 or 8 samples, every target with as many fragments, the standard sample locations, MSAA_ENABLE) render into host-only multisampled color images and multisampled depth surfaces; the pipeline rasterizes with that sample count. A CB_COLOR_CONTROL resolve over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image. A zero fill over a target's CMASK fast-clears it to the CB_COLOR_CLEAR_WORD values of its next use. EQAA, coverage conversion, nonstandard sample locations, sampling a multisampled surface and multisampled mips stay refused. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../src/Driver/Memory/BufferFill.cpp | 6 +- .../src/Driver/Packets/PacketMemory.cpp | 7 +- .../Execution/src/VulkanDevice.cpp | 2 + .../Graphics/include/MultisampleTarget.hpp | 20 ++ .../Graphics/include/Pipeline.hpp | 3 +- .../Graphics/include/State.hpp | 17 +- .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 7 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 37 ++- .../Graphics/src/MultisampleTarget.cpp | 262 ++++++++++++++++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 24 +- .../libSceAgcDriver/Graphics/src/State.cpp | 93 ++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 54 ++++ 15 files changed, 500 insertions(+), 39 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 675ae9ca6..349f1ea3d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -173,6 +173,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.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 ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -841,6 +842,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/DepthSurfaceReuse.cpp Graphics/src/Draw.cpp Graphics/src/DepthSurface.cpp + Graphics/src/MultisampleTarget.cpp Graphics/src/Pipeline.cpp Graphics/src/PipelineLibrary.cpp Graphics/src/State.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 5e7f9a53c..b8e7adb77 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -265,7 +266,10 @@ 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]) Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) { + Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); + } if (profile) { const auto now = std::chrono::steady_clock::now(); const auto cover = static_cast(coverage.cover); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index c7b1198a1..836410324 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -60,7 +61,11 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (opcode == 0x50 && packet.size() >= 7) { - if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) Graphics::NoteDepthMetadataFill(packet[4] | (static_cast(packet[5]) << 32u), packet[6] & 0x3ffffffu, packet[2]); + if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + Graphics::NoteColorMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + } } if (!drainAll && !endOfPipeInterrupt && (opcode == 0x49 || opcode == 0x37)) { if (const auto label = Pm4::DecodeLabelWrite(packet)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87d211a49..b41b2bdb3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -598,6 +599,7 @@ struct VulkanDevice::State { // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); Graphics::ClearDepthSurfaces(device); + Graphics::ClearMultisampleTargets(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp new file mode 100644 index 000000000..58ec1c2fb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +class StorageTexture; + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +void ClearMultisampleTargets(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 5258bc026..bb817d437 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -45,7 +45,7 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); + std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent = {}); // Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const; // The same inside a render pass another pipeline of the same attachments began (compatible by @@ -64,6 +64,7 @@ private: struct CachedFramebuffer { std::vector views; std::vector> owners; + std::vector owned; VkExtent2D extent; std::shared_ptr framebuffer; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 0012dbfa5..f02127656 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -60,6 +60,8 @@ struct ColorTarget { std::uint32_t exportIndex = 0; bool uintExport = false; std::uint32_t pipeBankXor = 0; + std::uint32_t samples = 1; + std::uint64_t cmaskAddress = 0; }; struct DepthTarget { @@ -71,6 +73,7 @@ struct DepthTarget { std::uint8_t clearStencil; std::uint64_t htileAddress = 0; bool htileStencil = false; + std::uint32_t samples = 1; }; struct State { @@ -112,6 +115,7 @@ struct State { VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; + std::uint32_t samples = 1; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); @@ -123,9 +127,10 @@ std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve }; Mode mode; std::vector targets; + std::optional source; }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); @@ -166,16 +171,18 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, - // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, - // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. + // PA_SU_VTX_CNTL, PA_SU_POLY_OFFSET_*, PA_SC_AA_CONFIG, the sample locations, the sample masks, + // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, CMASK_BASE_EXT, DCC_BASE_EXT, + // ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5e71b21d6..6f79dffc6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -38,6 +38,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear); // A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload // still writes it (see the snapshot constructor), so the texture may go before the batch completes. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b29e8d1f7..0b5d5eb79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -39,9 +39,9 @@ public: info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = VK_SAMPLE_COUNT_1_BIT; + info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -113,6 +113,7 @@ public: key[10] = components.b; key[11] = components.a; if (const auto found = textures.find(key); found != textures.end()) return found->second; + Require(target.samples == 1, "sampling a multisampled depth surface is not implemented"); 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; @@ -158,7 +159,7 @@ private: }; 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; + 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; } std::mutex& surfacesMutex() { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b7454391d..219430bd4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -1701,7 +1702,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4); } auto images = resources.StorageImages(); - for (const auto& target : record.targets) images.emplace_back(target->Image(), true); + for (const auto& target : record.targets) { + if (target != nullptr) images.emplace_back(target->Image(), true); + } recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()}); } std::unique_ptr scratch; @@ -1866,6 +1869,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& std::vector original; TileMipLayout mip{}; std::unique_ptr target; + bool multisampled = false; // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. std::unique_ptr dump; }; @@ -1879,6 +1883,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.color = state.colors[index]; const auto& color = binding.color; binding.gpuTiling = color.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr; + if (color.samples > 1) { + binding.multisampled = true; + timer.phase(PhaseSetup); + targetViews.push_back(MultisampleTargetView(context, color)); + timer.phase(PhaseReadTarget); + continue; + } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; @@ -1953,7 +1964,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); - const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr; }); + const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; const auto& contentKey = resolved.contentKey; @@ -2064,6 +2075,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; const bool lean = recorded && !drawTransitions; + const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); @@ -2084,8 +2097,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. std::vector> owners; owners.reserve(targets.size()); - for (const auto& binding : targets) owners.push_back(binding.resident); - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent); + std::vector persistent; + persistent.reserve(targets.size()); + for (const auto& binding : targets) { + owners.push_back(binding.resident); + persistent.push_back(binding.multisampled ? 1u : 0u); + } + auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2109,7 +2127,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()) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2498,6 +2516,15 @@ std::shared_ptr metadataPassResident(const Context& context, con } void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { + if (pass.mode == ColorMetadataPass::Mode::Resolve) { + Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination"); + const auto& destination = pass.targets.front(); + Require(destination.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented"); + const auto resident = CachedStorageSurface(context, SurfaceForTarget(destination)); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp new file mode 100644 index 000000000..22860840d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -0,0 +1,262 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::uint64_t cmaskBytes(VkExtent2D extent) { + const auto tiles = static_cast((extent.width + 7u) / 8u) * ((extent.height + 7u) / 8u); + return (tiles + 1u) / 2u; +} + +bool sameTarget(const ColorTarget& a, const ColorTarget& b) { + return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; +} + +VkClearColorValue fastClearColor(const ColorTarget& target) { + Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); + std::array texels{}; + for (std::size_t offset = 0; offset < texels.size(); offset += target.elementBytes) std::memcpy(texels.data() + offset, target.clearWords.data(), target.elementBytes); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "the fast-clear color of a multisampled target has no exact clear value in its format"); + return clear; +} + +template +void recordCommands(const Context& context, Record&& record) { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder); + if (batch) batch->SubmitAndWait(); +} + +class MultisampleTarget { +public: + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target) { + this->context.bufferPool.reset(); + const auto samples = static_cast(target.samples); + Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VkImageFormatProperties supported{}; + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target"); + 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); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearColorValue zero{}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } catch (...) { + release(); + throw; + } + } + ~MultisampleTarget() { release(); } + MultisampleTarget(const MultisampleTarget&) = delete; + MultisampleTarget& operator=(const MultisampleTarget&) = delete; + + void ApplyFastClear(const ColorTarget& use) { + if (!fastCleared) return; + fastCleared = false; + const auto clear = fastClearColor(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + + VkRenderPass ResolvePass() { + if (resolvePass != VK_NULL_HANDLE) return resolvePass; + std::array attachments{}; + attachments[0].format = target.format; + attachments[0].samples = static_cast(target.samples); + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].format = target.format; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + for (auto& attachment : attachments) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + } + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_GENERAL}; + const VkAttachmentReference resolve{1, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + subpass.pResolveAttachments = &resolve; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = static_cast(attachments.size()); + info.pAttachments = attachments.data(); + info.subpassCount = 1; + info.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &resolvePass), "vkCreateRenderPass resolve"); + return resolvePass; + } + + Context context; + ColorTarget target; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkRenderPass resolvePass = VK_NULL_HANDLE; + bool fastCleared = false; + +private: + void release() noexcept { + if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +struct CmaskFill { + std::uint64_t address; + std::uint64_t bytes; +}; + +std::mutex& targetsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& targets() { + static std::vector> list; + return list; +} + +std::vector& earlyFills() { + static std::vector fills; + return fills; +} + +bool coversCmask(std::uint64_t address, std::uint64_t bytes, const ColorTarget& target) { + return target.cmaskAddress != 0 && target.cmaskAddress >= address && target.cmaskAddress + cmaskBytes(target.extent) <= address + bytes; +} + +MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { + for (const auto& target : targets()) { + if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + target->ApplyFastClear(use); + return *target; + } + targets().push_back(std::make_unique(context, use)); + auto& created = *targets().back(); + auto& fills = earlyFills(); + created.fastCleared = std::any_of(fills.begin(), fills.end(), [&](const CmaskFill& fill) { return coversCmask(fill.address, fill.bytes, use); }); + created.ApplyFastClear(use); + return created; +} + +} + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + return acquire(context, target).view; +} + +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { + std::lock_guard lock(targetsMutex()); + auto& target = acquire(context, source); + const std::array views{target.view, destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(), views, source.extent); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = target.ResolvePass(); + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, source.extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + if (recorder != nullptr) { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(framebuffer); + } + }); + destination.MarkDirty(); +} + +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + std::lock_guard lock(targetsMutex()); + auto& fills = earlyFills(); + std::erase_if(fills, [&](const CmaskFill& fill) { return fill.address < address + bytes && address < fill.address + fill.bytes; }); + bool covered = false; + for (const auto& target : targets()) { + if (!coversCmask(address, bytes, target->target)) continue; + covered = true; + static std::atomic reports{0}; + if (pattern == 0 && !target->fastCleared && reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); + target->fastCleared = pattern == 0; + } + constexpr std::size_t remembered = 64; + if (pattern == 0 && !covered && fills.size() < remembered) fills.push_back({address, bytes}); +} + +void ClearMultisampleTargets(VkDevice device) { + std::lock_guard lock(targetsMutex()); + std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 43dbbbe4b..d4fbfec76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -209,7 +209,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput this->context.bufferPool.reset(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments"); + Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments"); @@ -270,7 +271,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = VK_SAMPLE_COUNT_1_BIT; + color.samples = static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -288,7 +289,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.depth) { VkAttachmentDescription depth{}; depth.format = state.depth->format; - depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.samples = static_cast(state.samples); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; @@ -363,7 +364,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.pNext = &provoking; } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + samples.rasterizationSamples = static_cast(state.samples); VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -482,21 +483,26 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { +std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent) { Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); - const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); + bool resident = true; + for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0)); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds // them (Kept keeps its framebuffer until the batch completes). std::erase_if(framebuffers, [](const CachedFramebuffer& entry) { - return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); }); + if (entry.framebuffer.use_count() != 1) return false; + for (std::size_t i = 0; i < entry.owners.size(); ++i) { + if (entry.owned[i] && entry.owners[i].expired()) return true; + } + return false; }); for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) { if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; // View handles are recycled once a StorageTexture is destroyed, so the owners must be the // very objects the views were made for. bool same = true; - for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get(); + for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owned[i] == (owners[i] != nullptr) && it->owners[i].lock().get() == owners[i].get(); if (!same) continue; std::rotate(it, std::next(it), framebuffers.end()); return framebuffers.back().framebuffer; @@ -514,6 +520,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span& key, const Context& context, const Stat append(key, state.blends.size()); for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); + append(key, state.samples); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b4b1e6ec3..629e2b9ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -199,7 +201,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; @@ -215,6 +217,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; + depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); @@ -302,6 +305,34 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { return (depthControl & 3u) == 0 ? depthControl & ~4u : depthControl; } +constexpr std::array, 2> StandardLocations2{{{4, 4}, {-4, -4}}}; +constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; +constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -8}}}; + +std::uint32_t decodeSamples(const Registers& cx) { + const auto config = read(cx, 0x2f8); + zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); + const auto log2 = config & 7u; + if (log2 == 0) { + Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); + return 1; + } + Require(log2 <= 3u, "more than eight samples are unsupported"); + Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); + const auto samples = 1u << log2; + const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = read(cx, 0x2fe + pixel * 4u + sample / 4u); + const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; + const auto x = static_cast(static_cast(bits << 4u)) >> 4; + const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; + Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + } + } + return samples; +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); @@ -579,6 +610,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + result.samples = decodeSamples(cx); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); @@ -606,8 +638,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { 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"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); - zero(cx, 0x292, ScanModeMask, "scan conversion mode"); + Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); + zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); + Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); @@ -669,6 +702,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported"); color.exportIndex = index; color.uintExport = slotExport == 7; + Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; @@ -851,17 +885,28 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap); const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); + const auto attrib = read(cx, 0x31d + stride); + zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes"); + color.samples = 1u << ((attrib >> 12u) & 7u); + Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); + Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); + const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; + Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported"); const auto viewMip = (view >> 26u) & 0xfu; - zero(cx, 0x31d + stride, ~0x20000u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; + Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); + if (color.samples > 1 && (info & 0x4000u) != 0) { + const auto cmaskHigh = find(cx, 0x398 + slot); + color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + } Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); @@ -945,11 +990,14 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto control = find(cx, 0x202); if (control == cx.end()) return std::nullopt; const auto mode = (control->second >> 4u) & 7u; - if (mode != 2u && mode != 6u) return std::nullopt; - ColorMetadataPass pass{mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}}; + const auto aaConfig = find(cx, 0x2f8); + const bool resolve = mode == 3u && aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + 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((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); + const auto samples = resolve ? decodeSamples(cx) : 1u; + if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); @@ -962,16 +1010,30 @@ std::optional DecodeColorMetadataPass(const QueueState& queue intersect(covered, cx, 0x81, false); intersect(covered, cx, 0x90, false); if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); - const auto targetMask = read(cx, 0x8e); - for (std::uint32_t slot = 0; slot < 8; ++slot) { - if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + const auto requireCovered = [&](const ColorTarget& target) { const auto width = static_cast(target.extent.width); const auto height = static_cast(target.extent.height); const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height; const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height; Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target"); + }; + if (resolve) { + const auto source = DecodeColorBuffer(cx, 0); + const auto destination = DecodeColorBuffer(cx, 1); + Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); + Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + requireCovered(destination); + pass.source = source; + pass.targets.push_back(destination); + return pass; + } + const auto targetMask = read(cx, 0x8e); + for (std::uint32_t slot = 0; slot < 8; ++slot) { + if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; + const auto target = DecodeColorBuffer(cx, slot); + Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + requireCovered(target); pass.targets.push_back(target); } return pass; @@ -1024,8 +1086,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { 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 (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; + if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e9558ef4c..5974f433f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1265,6 +1265,10 @@ bool ClearKeepsDenormals(const Context& context, VkFormat format) { return keeps; } +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { + return FillClearColor(format, elementBytes, pattern, clear); +} + VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const { Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture"); Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 111bf9b1c..b6f38a7f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1658,6 +1658,59 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +void multisampleTests() { + using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorMetadataPass; + using AgcDriver::Graphics::DecodeState; + const auto multisampled = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + auto queue = multisampled(); + auto state = DecodeState(queue); + Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection); + queue.context[0x302] = 0x44cc; + expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); + queue = multisampled(); + queue.context[0x31d] = 0x1000; + expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "sample count differs"); + queue = multisampled(); + queue.context[0x292] = 2; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); + queue = multisampled(); + queue.context[0x2f8] |= 0x10; + expectFailure([&] { DecodeState(queue); }, "coverage conversion"); + queue = multisampled(); + queue.context[0x31c] |= 0x6000; + queue.context[0x31f] = 0x1234; + Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode"); + queue = makeState(); + queue.context[0x31c] |= 0x4000; + expectFailure([&] { DecodeState(queue); }, "color compression"); + + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + auto pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); + queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -4362,6 +4415,7 @@ int main(int argc, char** argv) { cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); + multisampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); From 163947ef7226a2dba66e16020f8671981125de42 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Wed, 7 Oct 2026 18:17:38 +0200 Subject: [PATCH 252/277] style(agc): drop code comments the conventions check rejects Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f02127656..f09250a94 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -178,11 +178,9 @@ inline constexpr std::array DrawKeyRegisters{{ // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, - // PA_SU_VTX_CNTL, PA_SU_POLY_OFFSET_*, PA_SC_AA_CONFIG, the sample locations, the sample masks, - // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. + // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, - // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, CMASK_BASE_EXT, DCC_BASE_EXT, - // ATTRIB2, ATTRIB3. + // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; From eeefb06549217567ee7c726841361c3682d0e649 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Thu, 8 Oct 2026 18:32:58 +0200 Subject: [PATCH 253/277] fix(agc): multisampled targets use main's CMASK fields; their CMASK state refuses what it cannot model The duplicate ColorTarget::cmaskAddress, decode and size helper are gone: multisampled targets use main's cmaskAddress/cmaskBytes (whole metablocks). Single-sample targets keep main's rule (CMASK under FAST_CLEAR); a multisampled one takes it under FAST_CLEAR or COMPRESSION. A zero fill of the whole CMASK clears at the next use, 0xFFFFFFFF marks it expanded, any other or partial fill refuses the next use; eliminate/DCC passes over a multisampled target and a CB resolve into a fast-clear destination are refused. The draw key adds CMASK_BASE_EXT (0x398), which main decodes but did not key. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/MultisampleTarget.hpp | 20 +++++ .../Graphics/include/State.hpp | 2 +- .../Graphics/src/MultisampleTarget.cpp | 87 +++++++++++-------- .../libSceAgcDriver/Graphics/src/State.cpp | 24 ++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 66 ++++++++++++++ docs/dev/TechnicalDebt.md | 5 ++ 6 files changed, 158 insertions(+), 46 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 58ec1c2fb..f0c327c0b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -2,14 +2,34 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include namespace AgcDriver::Graphics { class StorageTexture; +class MultisampledCmask { +public: + MultisampledCmask() = default; + MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {} + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + void SeedKeys(DccKeys seeded); + void CheckAddress(std::uint64_t cmaskAddress) const; + bool TakeClear(); + DccKeys Keys() const { return keys; } + const std::string& Refusal() const { return refusal; } + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + DccKeys keys = DccKeys::Uncompressed; + std::string refusal; +}; + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f09250a94..bb18791e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -45,6 +45,7 @@ struct ColorTarget { bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; std::size_t cmaskBytes = 0; + bool cmaskFastClear = false; std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; @@ -61,7 +62,6 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; - std::uint64_t cmaskAddress = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 22860840d..667b87112 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -16,11 +16,6 @@ namespace AgcDriver::Graphics { namespace { -std::uint64_t cmaskBytes(VkExtent2D extent) { - const auto tiles = static_cast((extent.width + 7u) / 8u) * ((extent.height + 7u) / 8u); - return (tiles + 1u) / 2u; -} - bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } @@ -47,7 +42,7 @@ void recordCommands(const Context& context, Record&& record) { class MultisampleTarget { public: - MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target) { + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) { this->context.bufferPool.reset(); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); @@ -109,8 +104,7 @@ public: MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - if (!fastCleared) return; - fastCleared = false; + if (!cmask.TakeClear()) return; const auto clear = fastClearColor(use); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); @@ -160,7 +154,7 @@ public: VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; - bool fastCleared = false; + MultisampledCmask cmask; private: void release() noexcept { @@ -171,11 +165,6 @@ private: } }; -struct CmaskFill { - std::uint64_t address; - std::uint64_t bytes; -}; - std::mutex& targetsMutex() { static std::mutex mutex; return mutex; @@ -186,31 +175,66 @@ std::vector>& targets() { return list; } -std::vector& earlyFills() { - static std::vector fills; - return fills; -} - -bool coversCmask(std::uint64_t address, std::uint64_t bytes, const ColorTarget& target) { - return target.cmaskAddress != 0 && target.cmaskAddress >= address && target.cmaskAddress + cmaskBytes(target.extent) <= address + bytes; -} - MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { 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)); auto& created = *targets().back(); - auto& fills = earlyFills(); - created.fastCleared = std::any_of(fills.begin(), fills.end(), [&](const CmaskFill& fill) { return coversCmask(fill.address, fill.bytes, use); }); + if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; } } +void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the CMASK 0x%llx+0x%llx of a multisampled color target, leaving its tiles mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + if (pattern == 0) { + keys = DccKeys::Clear0000; + } else if (pattern == 0xffffffffu) { + keys = DccKeys::Uncompressed; + } else { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast(address), static_cast(bytes), pattern); + refusal = text; + } +} + +void MultisampledCmask::SeedKeys(DccKeys seeded) { + if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) { + keys = seeded; + return; + } + char text[192]; + std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast(address), DccKeysName(seeded)); + refusal = text; +} + +void MultisampledCmask::CheckAddress(std::uint64_t cmaskAddress) const { + if (cmaskAddress == address) return; + char text[192]; + std::snprintf(text, sizeof(text), "a multisampled color target used CMASK 0x%llx after its first use with CMASK 0x%llx, which is not modeled", static_cast(cmaskAddress), static_cast(address)); + Require(false, std::string(text)); +} + +bool MultisampledCmask::TakeClear() { + Require(refusal.empty(), refusal); + if (keys != DccKeys::Clear0000) return false; + keys = DccKeys::Uncompressed; + return true; +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); return acquire(context, target).view; @@ -240,18 +264,13 @@ 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()); - auto& fills = earlyFills(); - std::erase_if(fills, [&](const CmaskFill& fill) { return fill.address < address + bytes && address < fill.address + fill.bytes; }); - bool covered = false; for (const auto& target : targets()) { - if (!coversCmask(address, bytes, target->target)) continue; - covered = true; + const auto before = target->cmask.Keys(); + target->cmask.NoteFill(address, bytes, pattern); + if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; static std::atomic reports{0}; - if (pattern == 0 && !target->fastCleared && reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); - target->fastCleared = pattern == 0; + if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); } - constexpr std::size_t remembered = 64; - if (pattern == 0 && !covered && fills.size() < remembered) fills.push_back({address, bytes}); } void ClearMultisampleTargets(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 629e2b9ba..b66fae1e8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -903,10 +903,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); - if (color.samples > 1 && (info & 0x4000u) != 0) { - const auto cmaskHigh = find(cx, 0x398 + slot); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - } Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); @@ -958,15 +954,19 @@ 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) { - Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 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)"); - Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); const auto cmaskHigh = find(cx, 0x398 + slot); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); - color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); - GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported"); + if (cmask != 0) { + color.cmaskAddress = cmask; + color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); + if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + } + color.cmaskFastClear = (info & 0x2000u) != 0; } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -1022,6 +1022,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto destination = DecodeColorBuffer(cx, 1); Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); requireCovered(destination); pass.source = source; @@ -1032,6 +1033,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; const auto target = DecodeColorBuffer(cx, slot); + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); requireCovered(target); pass.targets.push_back(target); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b6f38a7f9..647f20dee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7,6 +7,8 @@ #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/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" @@ -1668,6 +1670,7 @@ void multisampleTests() { queue.context[0x292] = 3; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; queue.context[0x31d] = 0x9000; + queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; @@ -1697,6 +1700,28 @@ void multisampleTests() { queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0x1234; + const auto fastCleared = DecodeState(queue).colors[0]; + Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x4000; + queue.context[0x31f] = 0x1234; + const auto compressed = DecodeState(queue).colors[0]; + Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0; + expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0x1234; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + static_cast(DecodeState(queue)); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset)); queue = multisampled(); queue.context[0x0] = 0; @@ -1709,6 +1734,47 @@ void multisampleTests() { Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + queue.context[0x32b] = queue.context[0x31c] | 0x2000u; + queue.context[0x32e] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 8u); + queue.context[0x399] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 40u); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0020; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + + using AgcDriver::Graphics::DccKeys; + using AgcDriver::Graphics::MultisampledCmask; + MultisampledCmask cmask(0x10000, 0x1000); + cmask.NoteFill(0x10000, 0x1000, 0x11111111u); + expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed"); + cmask.NoteFill(0x10800, 0x1000, 0); + expectFailure([&] { cmask.TakeClear(); }, "partly covers"); + cmask.NoteFill(0x10000, 0x1000, 0); + Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once"); + cmask.NoteFill(0x10000, 0x1000, 0xffffffffu); + Require(!cmask.TakeClear(), "an expanded CMASK cleared"); + cmask.NoteFill(0x20000, 0x1000, 0x11111111u); + Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK"); + cmask.CheckAddress(0x10000); + expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); + MultisampledCmask seeded(0x30000, 0x1000); + seeded.SeedKeys(DccKeys::Clear0000); + Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + MultisampledCmask mixedSeed(0x30000, 0x1000); + mixedSeed.SeedKeys(DccKeys::Mixed); + expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); + MultisampledCmask unaddressed; + unaddressed.NoteFill(0, 0x1000, 0); + Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); } void DepthClipTests() { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 48a8cd098..c01cc5c59 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -519,6 +519,11 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them 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. - [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. +- [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. +- [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. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. 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 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. - [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. From ae6dfd9073b3fc020350b4c2b9a17368daf44ab1 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 09:59:05 +0200 Subject: [PATCH 254/277] fix(agc): the standard 8x pattern's last sample is at (7, -7) Vulkan's standard 8x sample 7 is (0.9375, 0.0625), (7, -7) in signed sixteenths; the table had (7, -8), so the standard pattern (PPSA99902's PA_SC_AA_SAMPLE_LOCS words 0xbd153fd1 / 0x9773f95b) was refused as nonstandard. Regression row with those words. Reported by @Amoenus. Co-Authored-By: Amoenus <5391396+Amoenus@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b66fae1e8..eb0ca112c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -307,7 +307,7 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { constexpr std::array, 2> StandardLocations2{{{4, 4}, {-4, -4}}}; constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; -constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -8}}}; +constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 647f20dee..3b27e5ac0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1683,6 +1683,15 @@ void multisampleTests() { queue.context[0x302] = 0x44cc; expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); queue = multisampled(); + queue.context[0x2f8] = 0x00300003; + 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; + } + state = DecodeState(queue); + Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; From b64da7caf733ee1734f10693477259f36cee0857 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 16:32:47 +0200 Subject: [PATCH 255/277] fix(agc): GPL fragment libraries keyed by the sample count The fragment shader and fragment output libraries are built with the pipeline's rasterizationSamples, so a 1x and a 4x pipeline with the same shaders and formats must not share them. Reported by adriadam10 (found by Raghav314 in #2242). Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d4fbfec76..27bfd7ee9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -186,6 +186,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias); appendKey(keys.preRasterization, state.stages.tessellation.has_value()); if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); + appendKey(keys.fragmentShader, state.samples); + appendKey(keys.fragmentOutput, state.samples); for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0c7912500..15f75e4de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5040,6 +5040,13 @@ void pipelineLibraryTests(const Device& device) { state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT; static_cast(lookup()); Require(counters().built == std::array{2, 2, 3, 3}, "another target format rebuilt more than the fragment output library"); + state.samples = 4; + state.colors.front().samples = 4; + const auto multisampled = lookup(); + Require(counters().built == std::array{2, 2, 4, 4}, "a 4x pipeline shared the 1x fragment shader or fragment output library"); + state.samples = 1; + state.colors.front().samples = 1; + Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); From d109f3966fe82b0d12dd4d6bae711f66714ad220 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 16:37:12 +0200 Subject: [PATCH 256/277] fix(agc): guest access to a multisampled color target's memory throws A multisampled target's samples are only on the host, so its guest memory never holds them: a CPU read through GuestMemory::Read/ReadCommitted, a DMA_DATA copy from it and a texture or storage image lookup at its address throw instead of returning bytes that were never written there. Asked by oneandonlydean; the test reads a resolved-only target's memory each way. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/include/GuestMemory.hpp | 1 + .../src/Driver/Packets/PacketMemory.cpp | 1 + .../Execution/src/GuestMemory.cpp | 8 ++ .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 51 +++++++++ .../Graphics/src/ShaderResources.cpp | 3 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 105 ++++++++++++++++++ 7 files changed, 170 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index ba201ca15..5321424ce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -183,6 +183,7 @@ unsigned long long CodeOffset(const void* address); // every read and store through this interface calls first. Code that reads guest memory directly // calls FlushGpuWrites itself. void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes)); +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)); void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd // while the call stores its page states, moved past it after, so a reader that loads an even diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 836410324..a2ba8abae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -61,6 +61,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (opcode == 0x50 && packet.size() >= 7) { + if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy"); if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { const auto address = packet[4] | (static_cast(packet[5]) << 32u); Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f82616343..b764c499d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1751,12 +1751,19 @@ void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) { flushHook.store(hook, std::memory_order_release); } +std::atomic readGuard{nullptr}; + +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) { + readGuard.store(guard, std::memory_order_release); +} + void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes); } void Read(std::uint64_t address, std::span destination, std::size_t alignment) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -1773,6 +1780,7 @@ void Read(std::uint64_t address, std::span destination, std::size_t a void ReadCommitted(std::uint64_t address, std::span destination) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index f0c327c0b..5d9d14728 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -34,6 +34,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); 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 667b87112..da9e4f889 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1,3 +1,4 @@ +#include #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -165,6 +166,41 @@ private: } }; +struct GuardedRange { + VkDevice device; + std::uint64_t address; + std::uint64_t bytes; + std::uint32_t samples; +}; + +std::shared_mutex& guardedMutex() { + static std::shared_mutex mutex; + return mutex; +} + +std::vector& guarded() { + static std::vector ranges; + return ranges; +} + +std::atomic& anyGuarded() { + static std::atomic any{false}; + return any; +} + +void guardReads(std::uint64_t address, std::size_t bytes) { + RequireNoMultisampleTarget(address, bytes, "a CPU read"); +} + +void guard(const Context& context, const ColorTarget& target) { + const auto bytes = std::max(target.bytes, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); + std::unique_lock lock(guardedMutex()); + guarded().push_back({context.device, target.address, bytes, target.samples}); + anyGuarded().store(true, std::memory_order_release); + static std::once_flag installed; + std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); +} + std::mutex& targetsMutex() { static std::mutex mutex; return mutex; @@ -183,6 +219,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { return *target; } targets().push_back(std::make_unique(context, use)); + guard(context, use); auto& created = *targets().back(); if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); @@ -276,6 +313,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == 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); +} + +void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { + if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; + std::shared_lock lock(guardedMutex()); + for (const auto& range : guarded()) { + if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); + throw std::runtime_error(text); + } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 8ce6009bf..aacd4f292 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -513,6 +514,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); @@ -831,6 +833,7 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (DepthSurfaceAt(viewed.baseAddress)) { char text[112]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 15f75e4de..fe1a3164b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -38,6 +39,7 @@ #else #include #endif +#include #include #include #include @@ -4312,6 +4314,108 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) { + std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t targetBytes = side * side * 4u; + constexpr std::size_t bytes = 4u * 65536u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the multisampled target block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { + std::string message; + try { + access(); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message); + }; + ColorTarget target{}; + target.address = address; + target.surfaceAddress = address; + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.bytes = targetBytes; + target.samples = 4; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::array texels{}; + refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + binding.guestDescriptor = { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (9u << 28u), + 0u, + 0u, + 0u, + 0u, + }; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + refused([&] { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, compute); + }, "a texture read", "a texture read"); + AgcDriver::GuestMemory::Read(address + 2u * 65536u, texels); + ClearMultisampleTargets(context.device); + AgcDriver::GuestMemory::Read(address + 256u, texels); + std::cout << "Multisampled target guest access tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -5153,6 +5257,7 @@ int main(int argc, char** argv) { resourceReadTests(device, recorder); readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); + multisampleGuestAccessTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); From 5c5912c49e3710572a6740ee2a725579422bf19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:18:10 +0200 Subject: [PATCH 257/277] fix(agc): guard a multisampled target's whole sample planes; docs for host-only multisampled targets The guest-access guard covers CB bytes times samples (the single-sample layout size alone missed the later sample planes' tile padding), with a test row in that padding. Remove two TechnicalDebt lines duplicated by the rebase, document the guard bound and the unevicted targets, and add the multisampled targets to ARCHITECTURE.md. --- .../prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 ++++--- docs/dev/ARCHITECTURE.md | 1 + docs/dev/TechnicalDebt.md | 3 +-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index da9e4f889..743b469cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -193,7 +193,7 @@ void guardReads(std::uint64_t address, std::size_t bytes) { } void guard(const Context& context, const ColorTarget& target) { - const auto bytes = std::max(target.bytes, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); + const auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); std::unique_lock lock(guardedMutex()); guarded().push_back({context.device, target.address, bytes, target.samples}); anyGuarded().store(true, std::memory_order_release); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index fe1a3164b..227dddc11 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4321,8 +4321,8 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { return; } constexpr std::uint32_t side = 64; - constexpr std::size_t targetBytes = side * side * 4u; - constexpr std::size_t bytes = 4u * 65536u; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t bytes = 5u * 65536u; #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else @@ -4410,7 +4410,8 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); - AgcDriver::GuestMemory::Read(address + 2u * 65536u, texels); + 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); AgcDriver::GuestMemory::Read(address + 256u, texels); std::cout << "Multisampled target guest access tests passed\n"; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f72550b6..7572b0e12 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -86,6 +86,7 @@ flowchart LR - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - 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. ### Shader MODE register access diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c01cc5c59..737fbe794 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -519,11 +519,10 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them 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. - [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. -- [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. -- [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. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. 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 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) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [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. From 208b908238aa9daea8f532dba0e3d2aaf6618097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:26:42 +0200 Subject: [PATCH 258/277] test(agc): register the multisampled target guest-access test in ctest The test only ran in the full recorder binary, which no ctest entry starts. It now has its own mode and entry (agc_multisample_guest_access); with the guard back at the single-sample size its new padding row fails. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 349f1ea3d..e0f74d373 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -791,8 +791,10 @@ if(BUILD_TESTING) 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) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + 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) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES 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 227dddc11..a1ab66b71 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5230,6 +5230,10 @@ 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]) == "--multisample-only") { + multisampleGuestAccessTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; From 660e24eeac9b07e5e2cf4be4028f26d98e1f7a3b Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:09:51 -0400 Subject: [PATCH 259/277] perf(shader): compute v_pk_add_f16 and v_pk_mul_f16 with native f16 math v_pk_add_f16 and v_pk_mul_f16 computed each half in f32 and rounded it back to f16, then fixed the NaN, FP16_OVFL and clamp results on the bits. On a device with native f16 that rounds to nearest even and keeps f16 denormals (the Float16, RoundingModeRTE and DenormPreserve capabilities that #1252 already checks), the arithmetic is now one f16vec2 OpFAdd or OpFMul decorated NoContraction, and the same NaN, FP16_OVFL and clamp rules run on its result bits. The results don't change. f32 has at least 2 * 11 + 2 significand bits, so rounding an f16 sum or product to f32 and then to f16 gives the correctly rounded f16 result. Vulkan requires f16 OpFAdd and OpFMul to be correctly rounded, the module keeps f16 denormals, and any NaN they produce is replaced by the NaN the existing rules select. v_pk_fma_f16 and v_pk_fmac_f16 keep the f32 path: Vulkan gives GLSL Fma only the precision of OpFMul followed by OpFAdd, so a native f16 Fma may round twice. New test agc_driver_packed_float16_native compiles one shader with packed add, mul, fma and fmac (op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants) with and without Float16, checks that only add and mul use f16vec2 math, and compares every result bit for bit on random and special inputs (zeros, denormals, infinities, NaNs, values near the f16 maximum) in nine float modes. --- .../tests/execution/PackedFloat16Native.cpp | 206 ++++++++++++++++++ .../IntermediateRepresentation/IrOpcode.hpp | 2 + .../src/IrOpcode.cpp | 2 + .../src/MaskedSelectEliminator.cpp | 2 + core/shader/recompiler/Recompiler.cpp | 2 + .../SpirvBackend/SpirvEmitterInstructions.hpp | 2 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 24 +- .../SpirvBackend/src/SpirvFlowEmitter.cpp | 2 + .../Translation/InstructionTranslator.hpp | 1 + .../Translation/TranslationContext.hpp | 2 + .../Translation/src/FloatInstructions.cpp | 19 +- .../Translation/src/InstructionTranslator.cpp | 1 + 12 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp new file mode 100644 index 000000000..5d00b9a85 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -0,0 +1,206 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc0f400a, 0x18020b04, + 0xcc10400b, 0x18020b04, + 0xcc0e400c, 0x1c1a0b04, + 0xcc0f4a0d, 0x30020b04, + 0xcc10c00e, 0x18020d04, + 0xcc0e330f, 0x8c1a0b04, + 0x7e200306, 0x78200b04, + 0xcc0ec011, 0x3c1a0f04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_pk_add_f16 v10, v4, v5", + "v_pk_mul_f16 v11, v4, v5", + "v_pk_fma_f16 v12, v4, v5, v6", + "v_pk_add_f16 v13, v4, v5 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1]", + "v_pk_mul_f16 v14, v4, v6 clamp", + "v_pk_fma_f16 v15, v4, v5, v6 op_sel:[0,1,1] op_sel_hi:[1,0,0] neg_lo:[0,0,1] neg_hi:[1,1,0]", + "v_pk_fmac_f16 v16, v4, v5", + "v_pk_fma_f16 v17, v4, v7, v6 neg_lo:[1,0,0] clamp", +}; + +struct Mode { + const char* name; + std::optional floatMode; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, +}}; + +std::uint64_t Seed = 0x9e3779b97f4a7c15ull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 10u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa; + case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa; + case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa; + default: return bits & 0xffffu; + } +} + +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; +} + +struct F16x2Math { + bool add = false; + bool mul = false; + bool fma = false; +}; + +F16x2Math FindF16x2Math(std::span spirv) { + std::optional half; + std::optional pair; + F16x2Math math; + for (std::size_t offset = 5u; offset < spirv.size();) { + const std::uint32_t words = spirv[offset] >> 16u; + const std::uint32_t opcode = spirv[offset] & 0xffffu; + Require(words != 0u && offset + words <= spirv.size(), "packed f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat && spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + if (opcode == spv::OpTypeVector && half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } + offset += words; + } + return math; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + const auto supports = [&](spv::Capability capability) { + return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); + }; + if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { + std::puts("packed f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + return VulkanTestSkipped; + } + std::vector withoutFloat16; + std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; }); + auto emulatedTarget = target; + emulatedTarget.supportedCapabilities = withoutFloat16; + + const std::span code(Code); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + shaderTarget, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + const auto run = [&](const auto& result) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + return Output; + }; + + std::uint64_t compared = 0u; + for (const auto& mode : Modes) { + const auto native = compile(target, mode); + const auto emulated = compile(emulatedTarget, mode); + const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words()); + const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words()); + Require(nativeMath.add && nativeMath.mul, std::string("packed f16 native: ") + mode.name + " does not use f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("packed f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused"); + Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("packed f16 native: ") + mode.name + " uses f16vec2 math without Float16"); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (auto& word : Input) word = Half() | (Half() << 16u); + const auto nativeOutput = run(native); + const auto emulatedOutput = run(emulated); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t actual = nativeOutput[tid * Results + index]; + const std::uint32_t expected = emulatedOutput[tid * Results + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + Require(actual == expected, std::string("packed f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected)); + } + } + compared += Threads * Results * 2u; + } + } + std::printf("packed f16 native tests passed (%llu halves)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index e580b1118..d6141b191 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -141,6 +141,8 @@ enum class IrOpcode : std::uint16_t { CompositeExtractU32x4, PackHalf2x16, PackFloat2x16Rtz, + FPAdd16x2, + FPMul16x2, FPAbs32, FPNeg32, FPSaturate32, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 7abf9208a..ecd80d32a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -182,6 +182,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("CompositeExtractU32x4", U32, U32x4, U32), makeMeta("PackHalf2x16", U32, F32x2), makeMeta("PackFloat2x16Rtz", U32, F32, F32), + makeMeta("FPAdd16x2", U32, U32, U32), + makeMeta("FPMul16x2", U32, U32, U32), makeMeta("FPAbs32", F32, F32), makeMeta("FPNeg32", F32, F32), makeMeta("FPSaturate32", F32, F32), diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 601dedf05..51ec69506 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -60,6 +60,8 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::CompositeExtractU32x4: case IrOpcode::PackHalf2x16: case IrOpcode::PackFloat2x16Rtz: + case IrOpcode::FPAdd16x2: + case IrOpcode::FPMul16x2: case IrOpcode::BitFieldInsert: case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 7e5883eeb..c37ec986c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -173,6 +173,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled; + const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast(capability)) != request.target.supportedCapabilities.end(); }; + translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.floatMode = request.context.floatMode; translateOptions.inputInfo = inputInfo; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 97071e7ca..46e5934e6 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -13,6 +13,8 @@ std::uint32_t EmitConvertU32F32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF32S32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitCompositeExtractU64(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitPackFloat2x16Rtz(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitFPSaturate32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitIAdd64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitISub64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index e680dc906..705aa967d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1345,7 +1345,7 @@ static bool NativeF16Rte(const SpirvEmitterState& state) { return supported(spv::CapabilityFloat16) && supported(spv::CapabilityRoundingModeRTE) && supported(spv::CapabilityDenormPreserve); } -static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { +static void EmitNativeF16Modes(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityFloat16); state.module.EmitCapability(spv::CapabilityRoundingModeRTE); state.module.EmitCapability(spv::CapabilityDenormPreserve); @@ -1355,6 +1355,10 @@ static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::ui state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 16u); state.nativeF16ModesEmitted = true; } +} + +static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { + EmitNativeF16Modes(state); const auto u32 = TypeU32(state); const auto f16 = state.module.Type(spv::OpTypeFloat, 16u); const auto half = Unary(state, spv::OpFConvert, f16, value); @@ -1370,6 +1374,24 @@ static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::ui return Binary(state, spv::OpBitwiseOr, u32, sign, result); } +static std::uint32_t TypeF16x2(SpirvEmitterState& state) { + if (!NativeF16Rte(state)) FailEmit("packed f16 math needs Float16, RoundingModeRTE and DenormPreserve"); + EmitNativeF16Modes(state); + return state.module.Type(spv::OpTypeVector, state.module.Type(spv::OpTypeFloat, 16u), 2u); +} + +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto sum = Binary(state, spv::OpFAdd, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, sum)); +} + +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto product = Binary(state, spv::OpFMul, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, product)); +} + std::uint32_t EmitF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { if (NativeF16Rte(state)) return EmitNativeF32ToF16BitsRte(state, value); const auto u32 = TypeU32(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 59a389eb2..de7d5810c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -356,6 +356,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::CompositeExtractU32x4: return Invoke(EmitCompositeExtractU32x4, ctx, inst); case IrOpcode::PackHalf2x16: return Invoke(EmitPackHalf2x16, ctx, inst); case IrOpcode::PackFloat2x16Rtz: return Invoke(EmitPackFloat2x16Rtz, ctx, inst); + case IrOpcode::FPAdd16x2: return Invoke(EmitFPAdd16x2, ctx, inst); + case IrOpcode::FPMul16x2: return Invoke(EmitFPMul16x2, ctx, inst); case IrOpcode::FPAbs32: return Invoke(EmitFPAbs32, ctx, inst); case IrOpcode::FPNeg32: return Invoke(EmitFPNeg32, ctx, inst); case IrOpcode::FPSaturate32: return Invoke(EmitFPSaturate32, ctx, inst); diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index 4f5674034..c1aa7723a 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -33,6 +33,7 @@ struct TranslateOptions { std::uint32_t sharedMemoryBytes = 0; std::uint64_t shaderHash = 0; bool fragmentShaderBarycentricEnabled = false; + bool nativeFloat16 = false; std::optional floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 5c8139908..ed3e0da29 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -17,6 +17,7 @@ public: void TranslateInstruction(const RdnaInstruction& instruction); void SetPixelInput(const ShaderPixelInputInfo* info, bool barycentricEnabled) { pixelInput = info; fragmentShaderBarycentricEnabled = barycentricEnabled; } void SetFloatMode(const std::optional& mode) { floatMode = mode; ieeeMode = mode.has_value() && mode->ieeeMode; } + void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); @@ -344,6 +345,7 @@ private: bool fragmentShaderBarycentricEnabled = false; std::optional floatMode; bool ieeeMode = false; + bool nativeFloat16 = false; IrBuilder ir; IrBlock& block; IrU1 instructionBranchCondition; diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 60c51a60d..b19d72c94 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -40,8 +40,15 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc return bits; }; const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, laneBits(sourceAt(inst, 0u), high), laneBits(sourceAt(inst, 1u), high)); }; - IrU32 result = quietSnan ? packU16Lanes(minMaxLane(false), minMaxLane(true)) : packHalf2x16(translateLane(false), translateLane(true)); - if (!inst.destination.clamp || !dx10Clamp() || quietSnan) { + const bool native = nativeFloat16 && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); + const auto nativeResult = [&]() { + const auto packed = [&](const RdnaOperand& operand) { return packU16Lanes(laneBits(operand, false), laneBits(operand, true)); }; + const IrU32 lhs = packed(sourceAt(inst, 0u)); + const IrU32 rhs = packed(sourceAt(inst, 1u)); + return IrU32(ir.Emit(opcode == IrOpcode::FPAdd32 ? IrOpcode::FPAdd16x2 : IrOpcode::FPMul16x2, IrType::U32, {&lhs.Value(), &rhs.Value()})); + }; + IrU32 result = quietSnan ? packU16Lanes(minMaxLane(false), minMaxLane(true)) : native ? nativeResult() : packHalf2x16(translateLane(false), translateLane(true)); + if (native || !inst.destination.clamp || !dx10Clamp() || quietSnan) { const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; @@ -63,6 +70,14 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc bits[index] = laneBits(accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index), high); } IrU32 lane(high ? ir.ShiftRightLogical(result.Value(), ir.Constant(16u)) : ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu))); + if (native && inst.destination.clamp && dx10Clamp()) return lane; + if (native && fp16Overflow()) { + IrU1 overflow(ir.IEqual(magnitude(lane).Value(), ir.Constant(0x7c00u))); + for (std::uint32_t index = 0u; index < count; ++index) { + overflow = IrU1(ir.LogicalAnd(overflow.Value(), ir.INotEqual(magnitude(bits[index]).Value(), ir.Constant(0x7c00u)))); + } + lane = IrU32(ir.Select(overflow.Value(), ir.BitwiseOr(ir.BitwiseAnd(lane.Value(), ir.Constant(0x8000u)), ir.Constant(0x7bffu)), lane.Value())); + } if (quietSnan) { lane = IrU32(ir.Select(isNan(bits[0]).Value(), bits[1].Value(), lane.Value())); lane = IrU32(ir.Select(isNan(bits[1]).Value(), bits[0].Value(), lane.Value())); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 2539030a8..997aae94f 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -462,6 +462,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con TranslationContext context(program, *blocks[typedIndex], vectorLimit); context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); + context.SetNativeFloat16(options.nativeFloat16); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; if (isCodeTableLoad(cfg, instruction.programCounter)) { From 2ac8c7238370328b326025a21b0cdd1052d2517c Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:35:21 -0400 Subject: [PATCH 260/277] test(agc): bound packed f16 SPIR-V scanner reads FindF16x2Math read spirv[offset + 1] for every instruction once the f16 vector type was known, so a final one-word OpFunctionEnd read past the module. Check the opcode and its minimum word count before reading operands. --- .../tests/execution/PackedFloat16Native.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp index 5d00b9a85..092a17fd7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -116,12 +116,21 @@ F16x2Math FindF16x2Math(std::span spirv) { const std::uint32_t words = spirv[offset] >> 16u; const std::uint32_t opcode = spirv[offset] & 0xffffu; Require(words != 0u && offset + words <= spirv.size(), "packed f16 native: malformed SPIR-V"); - if (opcode == spv::OpTypeFloat && spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; - if (opcode == spv::OpTypeVector && half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; - if (pair && spirv[offset + 1u] == *pair) { - math.add = math.add || opcode == spv::OpFAdd; - math.mul = math.mul || opcode == spv::OpFMul; - math.fma = math.fma || opcode == spv::OpExtInst; + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "packed f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "packed f16 native: malformed vector type"); + if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + } + if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) { + Require(words >= 5u, "packed f16 native: malformed arithmetic instruction"); + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } } offset += words; } From d694aac7b4005b2f7628b9f435aea4e7598ea6da Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 16:14:19 +0300 Subject: [PATCH 261/277] fix(libkernel): translate every host socket error to the guest errno of its name NativeError listed 17 Winsock and 17 POSIX errors and turned every other one into EIO. A second connect while the first is pending (WSAEALREADY, EALREADY) and an option the host rejects (WSAENOPROTOOPT, ENOPROTOOPT) reached the guest as EIO; on Linux a nonblocking connect (EINPROGRESS), a send with no destination (EDESTADDRREQ), ENOTSOCK, EAFNOSUPPORT, EOPNOTSUPP and others did too. The translation moves to SocketError.hpp and covers the socket errors of both hosts by name, with the FreeBSD 11 numbers. WSAESHUTDOWN stays EPIPE as before. socketpair's own table is gone, as NativeError covers it. (cherry picked from commit a889c98e6a59d62da289f2e6a77355d2b41a1bde) --- core/libs/prx/libkernel/CMakeLists.txt | 4 + .../libkernel/Socket/include/SocketError.hpp | 99 +++++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 84 +++---------- core/libs/tests/GuestSocketError.cpp | 119 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 239 insertions(+), 68 deletions(-) create mode 100644 core/libs/prx/libkernel/Socket/include/SocketError.hpp create mode 100644 core/libs/tests/GuestSocketError.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6fa23e5fe..ea21b1c25 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -796,6 +796,10 @@ if(BUILD_TESTING) set_tests_properties(guest_system_command PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketError.cpp) + 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_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/SocketError.hpp b/core/libs/prx/libkernel/Socket/include/SocketError.hpp new file mode 100644 index 000000000..a7438c651 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketError.hpp @@ -0,0 +1,99 @@ +#pragma once +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace GuestSockets { + +constexpr int GuestErrorIo = 5; + +constexpr int GuestErrorFromHost(int hostError) { + switch (hostError) { +#ifdef _WIN32 + case WSAEINTR: return 4; + case WSAEBADF: return 9; + case WSAEACCES: return 13; + case WSAEFAULT: return 14; + case WSAEINVAL: return 22; + case WSAEMFILE: return 24; + case WSAEWOULDBLOCK: return 35; + case WSAEINPROGRESS: return 36; + case WSAEALREADY: return 37; + case WSAENOTSOCK: return 38; + case WSAEDESTADDRREQ: return 39; + case WSAEMSGSIZE: return 40; + case WSAEPROTOTYPE: return 41; + case WSAENOPROTOOPT: return 42; + case WSAEPROTONOSUPPORT: return 43; + case WSAESOCKTNOSUPPORT: return 44; + case WSAEOPNOTSUPP: return 45; + case WSAEPFNOSUPPORT: return 46; + case WSAEAFNOSUPPORT: return 47; + case WSAEADDRINUSE: return 48; + case WSAEADDRNOTAVAIL: return 49; + case WSAENETDOWN: return 50; + case WSAENETUNREACH: return 51; + case WSAENETRESET: return 52; + case WSAECONNABORTED: return 53; + case WSAECONNRESET: return 54; + case WSAENOBUFS: return 55; + case WSAEISCONN: return 56; + case WSAENOTCONN: return 57; + case WSAESHUTDOWN: return 32; + case WSAETOOMANYREFS: return 59; + case WSAETIMEDOUT: return 60; + case WSAECONNREFUSED: return 61; + case WSAEHOSTDOWN: return 64; + case WSAEHOSTUNREACH: return 65; +#else + case EPERM: return 1; + case EINTR: return 4; + case EBADF: return 9; + case ENOMEM: return 12; + case EACCES: return 13; + case EFAULT: return 14; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EPIPE: return 32; + case EAGAIN: return 35; +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: return 35; +#endif + case EINPROGRESS: return 36; + case EALREADY: return 37; + case ENOTSOCK: return 38; + case EDESTADDRREQ: return 39; + case EMSGSIZE: return 40; + case EPROTOTYPE: return 41; + case ENOPROTOOPT: return 42; + case EPROTONOSUPPORT: return 43; + case ESOCKTNOSUPPORT: return 44; + case EOPNOTSUPP: return 45; + case EPFNOSUPPORT: return 46; + case EAFNOSUPPORT: return 47; + case EADDRINUSE: return 48; + case EADDRNOTAVAIL: return 49; + case ENETDOWN: return 50; + case ENETUNREACH: return 51; + case ENETRESET: return 52; + case ECONNABORTED: return 53; + case ECONNRESET: return 54; + case ENOBUFS: return 55; + case EISCONN: return 56; + case ENOTCONN: return 57; + case ESHUTDOWN: return 58; + case ETOOMANYREFS: return 59; + case ETIMEDOUT: return 60; + case ECONNREFUSED: return 61; + case EHOSTDOWN: return 64; + case EHOSTUNREACH: return 65; + case EPROTO: return 92; +#endif + default: return GuestErrorIo; + } +} + +} diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5c3791d1e..52f37a428 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #endif #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" @@ -54,47 +55,9 @@ int NativeSendFlags(int flags) { } int NativeError() { #ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEWOULDBLOCK: return 35; - case WSAEADDRINUSE: return 48; - case WSAEADDRNOTAVAIL: return 49; - case WSAEACCES: return 13; - case WSAEMSGSIZE: return 40; - case WSAENETUNREACH: return 51; - case WSAEHOSTUNREACH: return 65; - case WSAECONNRESET: return 54; - case WSAECONNABORTED: return 53; - case WSAEISCONN: return 56; - case WSAENOTCONN: return 57; - case WSAENOBUFS: return 55; - case WSAETIMEDOUT: return 60; - case WSAECONNREFUSED: return 61; - case WSAEINTR: return 4; - case WSAEINVAL: return 22; - case WSAESHUTDOWN: return 32; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(WSAGetLastError()); #else - switch (errno) { - case EAGAIN: return 35; - case EADDRINUSE: return 48; - case EADDRNOTAVAIL: return 49; - case EACCES: return 13; - case EMSGSIZE: return 40; - case ENETUNREACH: return 51; - case EHOSTUNREACH: return 65; - case ECONNREFUSED: return 61; - case ECONNRESET: return 54; - case ECONNABORTED: return 53; - case EISCONN: return 56; - case ENOTCONN: return 57; - case ENOBUFS: return 55; - case ETIMEDOUT: return 60; - case EINTR: return 4; - case EINVAL: return 22; - case EPIPE: return 32; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(errno); #endif } struct Socket { @@ -354,21 +317,6 @@ namespace { constexpr int GuestUnix = 1; constexpr int GuestCloseOnExec = 0x10000000; constexpr int GuestNonBlocking = 0x20000000; -int PairError() { -#ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEMFILE: return 24; - default: return NativeError(); - } -#else - switch (errno) { - case EMFILE: return 24; - case ENFILE: return 23; - case ENOMEM: return 12; - default: return NativeError(); - } -#endif -} int SetNonBlocking(Socket& socket) { #ifdef _WIN32 unsigned long value = 1; @@ -394,23 +342,23 @@ int NativePair(int type, NativeSocket (&pair)[2]) { loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK); const int nativeType = type == 1 ? SOCK_STREAM : SOCK_DGRAM; Socket first(::socket(AF_INET, nativeType, 0), 2, type); - if (first.value == Invalid) return PairError(); + if (first.value == Invalid) return NativeError(); const int exclusive = 1; setsockopt(first.value, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&exclusive), sizeof(exclusive)); - if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in firstName{}; - if (!LoopbackName(first.value, firstName)) return PairError(); + if (!LoopbackName(first.value, firstName)) return NativeError(); Socket second(::socket(AF_INET, nativeType, 0), 2, type); - if (second.value == Invalid) return PairError(); + if (second.value == Invalid) return NativeError(); if (type == 1) { - if (listen(first.value, 1)) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (listen(first.value, 1)) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); sockaddr_in peer{}; int peerSize = sizeof(peer); Socket accepted(accept(first.value, reinterpret_cast(&peer), &peerSize), 2, type); - if (accepted.value == Invalid) return PairError(); + if (accepted.value == Invalid) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); if (peer.sin_port != secondName.sin_port) return 53; const int noDelay = 1; setsockopt(second.value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&noDelay), sizeof(noDelay)); @@ -421,11 +369,11 @@ int NativePair(int type, NativeSocket (&pair)[2]) { accepted.value = Invalid; return 0; } - if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); - if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); + if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); pair[0] = first.value; pair[1] = second.value; first.value = Invalid; @@ -436,7 +384,7 @@ int NativePair(int type, NativeSocket (&pair)[2]) { int NativePair(int type, NativeSocket (&pair)[2]) { const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : SOCK_SEQPACKET; int native[2]; - if (::socketpair(AF_UNIX, nativeType, 0, native)) return PairError(); + if (::socketpair(AF_UNIX, nativeType, 0, native)) return NativeError(); pair[0] = native[0]; pair[1] = native[1]; return 0; diff --git a/core/libs/tests/GuestSocketError.cpp b/core/libs/tests/GuestSocketError.cpp new file mode 100644 index 000000000..62a1270fe --- /dev/null +++ b/core/libs/tests/GuestSocketError.cpp @@ -0,0 +1,119 @@ +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include +#include + +struct ErrorCase { + const char* name; + int host; + int guest; +}; + +#define HostError(prefix, name, guest) {#prefix #name, prefix##name, guest} + +#ifdef _WIN32 +static constexpr ErrorCase Cases[] = { + HostError(WSA, EINTR, 4), + HostError(WSA, EBADF, 9), + HostError(WSA, EACCES, 13), + HostError(WSA, EFAULT, 14), + HostError(WSA, EINVAL, 22), + HostError(WSA, EMFILE, 24), + HostError(WSA, EWOULDBLOCK, 35), + HostError(WSA, EINPROGRESS, 36), + HostError(WSA, EALREADY, 37), + HostError(WSA, ENOTSOCK, 38), + HostError(WSA, EDESTADDRREQ, 39), + HostError(WSA, EMSGSIZE, 40), + HostError(WSA, EPROTOTYPE, 41), + HostError(WSA, ENOPROTOOPT, 42), + HostError(WSA, EPROTONOSUPPORT, 43), + HostError(WSA, ESOCKTNOSUPPORT, 44), + HostError(WSA, EOPNOTSUPP, 45), + HostError(WSA, EPFNOSUPPORT, 46), + HostError(WSA, EAFNOSUPPORT, 47), + HostError(WSA, EADDRINUSE, 48), + HostError(WSA, EADDRNOTAVAIL, 49), + HostError(WSA, ENETDOWN, 50), + HostError(WSA, ENETUNREACH, 51), + HostError(WSA, ENETRESET, 52), + HostError(WSA, ECONNABORTED, 53), + HostError(WSA, ECONNRESET, 54), + HostError(WSA, ENOBUFS, 55), + HostError(WSA, EISCONN, 56), + HostError(WSA, ENOTCONN, 57), + HostError(WSA, ETOOMANYREFS, 59), + HostError(WSA, ETIMEDOUT, 60), + HostError(WSA, ECONNREFUSED, 61), + HostError(WSA, EHOSTDOWN, 64), + HostError(WSA, EHOSTUNREACH, 65), +}; +static constexpr int UnmappedHost = WSAEDISCON; +static constexpr int ShutdownHost = WSAESHUTDOWN; +#else +static constexpr ErrorCase Cases[] = { + HostError(, EPERM, 1), + HostError(, EINTR, 4), + HostError(, EBADF, 9), + HostError(, ENOMEM, 12), + HostError(, EACCES, 13), + HostError(, EFAULT, 14), + HostError(, EINVAL, 22), + HostError(, ENFILE, 23), + HostError(, EMFILE, 24), + HostError(, EPIPE, 32), + HostError(, EAGAIN, 35), + HostError(, EWOULDBLOCK, 35), + HostError(, EINPROGRESS, 36), + HostError(, EALREADY, 37), + HostError(, ENOTSOCK, 38), + HostError(, EDESTADDRREQ, 39), + HostError(, EMSGSIZE, 40), + HostError(, EPROTOTYPE, 41), + HostError(, ENOPROTOOPT, 42), + HostError(, EPROTONOSUPPORT, 43), + HostError(, ESOCKTNOSUPPORT, 44), + HostError(, EOPNOTSUPP, 45), + HostError(, EPFNOSUPPORT, 46), + HostError(, EAFNOSUPPORT, 47), + HostError(, EADDRINUSE, 48), + HostError(, EADDRNOTAVAIL, 49), + HostError(, ENETDOWN, 50), + HostError(, ENETUNREACH, 51), + HostError(, ENETRESET, 52), + HostError(, ECONNABORTED, 53), + HostError(, ECONNRESET, 54), + HostError(, ENOBUFS, 55), + HostError(, EISCONN, 56), + HostError(, ENOTCONN, 57), + HostError(, ESHUTDOWN, 58), + HostError(, ETOOMANYREFS, 59), + HostError(, ETIMEDOUT, 60), + HostError(, ECONNREFUSED, 61), + HostError(, EHOSTDOWN, 64), + HostError(, EHOSTUNREACH, 65), + HostError(, EPROTO, 92), +}; +static constexpr int UnmappedHost = ENOENT; +#endif + +int main() { + int failures = 0; + for (const auto& entry : Cases) { + const int guest = GuestSockets::GuestErrorFromHost(entry.host); + if (guest != entry.guest) { + std::fprintf(stderr, "%s (%d): guest %d, FreeBSD %d\n", entry.name, entry.host, guest, entry.guest); + ++failures; + } + } + if (GuestSockets::GuestErrorFromHost(UnmappedHost) != 5) { + std::fprintf(stderr, "a host error with no FreeBSD socket counterpart is not EIO\n"); + ++failures; + } +#ifdef _WIN32 + if (GuestSockets::GuestErrorFromHost(ShutdownHost) != 32) { + std::fprintf(stderr, "WSAESHUTDOWN is not EPIPE\n"); + ++failures; + } +#endif + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c9b805354..73a956600 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -576,6 +576,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `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 - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket 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`; a repeated one reports `EALREADY` on both - [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 - [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 - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. From d9c26226550e6bb989a5b1b2efb840307b8a0a23 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sun, 11 Oct 2026 16:34:49 +0300 Subject: [PATCH 262/277] docs(libkernel): record the unreported nonblocking connect refusal and the recv after SHUT_RD difference (cherry picked from commit c1519d951b2b77346392ccc18204f37c587e7eda) --- core/libs/tests/GuestSocketError.cpp | 7 +++++++ docs/dev/TechnicalDebt.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSocketError.cpp b/core/libs/tests/GuestSocketError.cpp index 62a1270fe..466bb1b8e 100644 --- a/core/libs/tests/GuestSocketError.cpp +++ b/core/libs/tests/GuestSocketError.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/Socket/include/SocketError.hpp" #include #include +#include struct ErrorCase { const char* name; @@ -109,6 +110,12 @@ int main() { std::fprintf(stderr, "a host error with no FreeBSD socket counterpart is not EIO\n"); ++failures; } + for (const int host : {0, -1}) { + if (GuestSockets::GuestErrorFromHost(host) != 5) { + std::fprintf(stderr, "host error %d is not EIO\n", host); + ++failures; + } + } #ifdef _WIN32 if (GuestSockets::GuestErrorFromHost(ShutdownHost) != 32) { std::fprintf(stderr, "WSAESHUTDOWN is not EPIPE\n"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 73a956600..25215bf54 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -576,7 +576,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [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. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `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 - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket 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`; a repeated one reports `EALREADY` on both +- [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 - [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 - [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 - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. From e8ea9a62bbc0f7d45007129bbb667268e8c35f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:43:47 +0200 Subject: [PATCH 263/277] test(libkernel): check that a socket call returns the mapped guest errno Sending on an unconnected UDP socket must report the guest EDESTADDRREQ (39) through NativeError, so a regression to EIO is caught on Linux. --- core/libs/tests/GuestUdp.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 6756fbace..2f69a2201 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -68,6 +68,13 @@ int main() { Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); +#ifndef _WIN32 + const int unconnected = socket_nid_postfix(2, 2, 0); + Require(unconnected >= 0); + Require(sendto_nid_postfix(unconnected, "x", 1, 0, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 39); + Require(close_nid_postfix(unconnected) == 0); +#endif const int unsized = socket_nid_postfix(2, 2, 0); std::array unsizedAddress{0, 2, 0, 0, 127, 0, 0, 1}; Require(unsized >= 0 && bind_nid_postfix(unsized, unsizedAddress.data(), unsizedAddress.size()) == 0); From a36b9092d165c621820c8dc26114ed2fa771aff7 Mon Sep 17 00:00:00 2001 From: Renan Pires Date: Sun, 11 Oct 2026 13:00:35 -0300 Subject: [PATCH 264/277] feat(libc): implement the long double math functions RetroArch imports --- core/libs/prx/libc/src/Math.cpp | 16 +++++++++++ core/libs/tests/GuestMath.cpp | 51 +++++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 68 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 23a813338..93d9b868f 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -168,6 +168,22 @@ double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } +long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } +long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } +long double APS5_VABI cosl_nid_postfix(long double x) { return std::cos(x); } +long double APS5_VABI tanl_nid_postfix(long double x) { return std::tan(x); } +long double APS5_VABI sinhl_nid_postfix(long double x) { return std::sinh(x); } +long double APS5_VABI coshl_nid_postfix(long double x) { return std::cosh(x); } +long double APS5_VABI tanhl_nid_postfix(long double x) { return std::tanh(x); } +long double APS5_VABI expl_nid_postfix(long double x) { return std::exp(x); } +long double APS5_VABI exp2l_nid_postfix(long double x) { return std::exp2(x); } +long double APS5_VABI logl_nid_postfix(long double x) { return std::log(x); } +long double APS5_VABI log10l_nid_postfix(long double x) { return std::log10(x); } +long double APS5_VABI powl_nid_postfix(long double base, long double exp) { return std::pow(base, exp); } +long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } +long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } +double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 273034aa4..88946360a 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -83,6 +83,22 @@ std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); +long double APS5_VABI atanl_nid_postfix(long double); +long double APS5_VABI sinl_nid_postfix(long double); +long double APS5_VABI cosl_nid_postfix(long double); +long double APS5_VABI tanl_nid_postfix(long double); +long double APS5_VABI sinhl_nid_postfix(long double); +long double APS5_VABI coshl_nid_postfix(long double); +long double APS5_VABI tanhl_nid_postfix(long double); +long double APS5_VABI expl_nid_postfix(long double); +long double APS5_VABI exp2l_nid_postfix(long double); +long double APS5_VABI logl_nid_postfix(long double); +long double APS5_VABI log10l_nid_postfix(long double); +long double APS5_VABI powl_nid_postfix(long double, long double); +long double APS5_VABI ldexpl_nid_postfix(long double, int); +long double APS5_VABI truncl_nid_postfix(long double); +double APS5_VABI logb_nid_postfix(double); +double APS5_VABI remainder_nid_postfix(double, double); } static void Require(bool value) { if (!value) std::abort(); } @@ -313,6 +329,40 @@ static void CheckAsin() { Require(!std::fetestexcept(FE_INVALID)); } +static void CheckExtendedMath() { + Require(ExtendedBits(atanl_nid_postfix(INFINITY), 0xc90fdaa22168c235ull, 0x3fff)); + Require(std::fabs(atanl_nid_postfix(1.0L) - 0.785398163397448309616L) < 1e-18L); + Require(std::fabs(sinl_nid_postfix(0.5L) - 0.479425538604203000273L) < 1e-18L); + Require(std::fabs(cosl_nid_postfix(0.5L) - 0.877582561890372716116L) < 1e-18L); + Require(std::fabs(tanl_nid_postfix(0.5L) - 0.546302489843790513255L) < 1e-18L); + Require(std::fabs(sinhl_nid_postfix(1.0L) - 1.17520119364380145688L) < 1e-18L); + Require(std::fabs(coshl_nid_postfix(1.0L) - 1.54308063481524377848L) < 1e-18L); + Require(std::fabs(tanhl_nid_postfix(0.5L) - 0.462117157260009758502L) < 1e-18L); + Require(std::fabs(expl_nid_postfix(1.0L) - 2.71828182845904523536L) < 1e-18L); + Require(std::fabs(logl_nid_postfix(2.0L) - 0.693147180559945309417L) < 1e-18L); + Require(sinl_nid_postfix(0.0L) == 0.0L && std::signbit(sinl_nid_postfix(-0.0L)) && cosl_nid_postfix(0.0L) == 1.0L); + Require(std::isnan(sinl_nid_postfix(INFINITY)) && std::isnan(cosl_nid_postfix(-INFINITY)) && std::isnan(tanl_nid_postfix(INFINITY))); + Require(sinhl_nid_postfix(-INFINITY) == -INFINITY && coshl_nid_postfix(-INFINITY) == INFINITY && tanhl_nid_postfix(INFINITY) == 1.0L); + Require(sinhl_nid_postfix(11358.0L) == INFINITY && std::isfinite(sinhl_nid_postfix(11356.0L)) && std::isfinite(coshl_nid_postfix(11356.0L))); + Require(expl_nid_postfix(0.0L) == 1.0L && expl_nid_postfix(-INFINITY) == 0.0L && expl_nid_postfix(11357.0L) == INFINITY); + Require(exp2l_nid_postfix(10.0L) == 1024.0L && exp2l_nid_postfix(-16446.0L) == 0.0L && exp2l_nid_postfix(16384.0L) == INFINITY); + Require(ExtendedBits(exp2l_nid_postfix(-16445.0L), 1, 0) && ExtendedBits(exp2l_nid_postfix(-16444.0L), 2, 0)); + Require(logl_nid_postfix(1.0L) == 0.0L && logl_nid_postfix(0.0L) == -INFINITY && std::isnan(logl_nid_postfix(-1.0L))); + Require(std::fabs(log10l_nid_postfix(1000.0L) - 3.0L) < 1e-18L && log10l_nid_postfix(1.0L) == 0.0L); + Require(log10l_nid_postfix(0.0L) == -INFINITY && std::isnan(log10l_nid_postfix(-1.0L))); + Require(powl_nid_postfix(2.0L, 64.0L) == 18446744073709551616.0L && powl_nid_postfix(-2.0L, 3.0L) == -8.0L); + Require(powl_nid_postfix(0.0L, -1.0L) == INFINITY && powl_nid_postfix(1.0L, NAN) == 1.0L && std::isnan(powl_nid_postfix(-2.0L, 0.5L))); + Require(ldexpl_nid_postfix(1.0L, 16383) == 0x1p16383L && ldexpl_nid_postfix(1.0L, 16384) == INFINITY && ldexpl_nid_postfix(3.0L, -2) == 0.75L); + Require(ExtendedBits(ldexpl_nid_postfix(1.0L, -16445), 1, 0) && ldexpl_nid_postfix(1.0L, -16446) == 0.0L); + Require(truncl_nid_postfix(2.7L) == 2.0L && truncl_nid_postfix(-2.7L) == -2.0L && ExtendedBits(truncl_nid_postfix(-0.5L), 0, 0x8000)); + Require(truncl_nid_postfix(9223372036854775807.5L) == 9223372036854775807.0L && truncl_nid_postfix(-INFINITY) == -INFINITY); + Require(logb_nid_postfix(8.0) == 3.0 && logb_nid_postfix(0.75) == -1.0 && logb_nid_postfix(0x1p-1074) == -1074.0); + Require(logb_nid_postfix(0.0) == -INFINITY && logb_nid_postfix(-INFINITY) == INFINITY && std::isnan(logb_nid_postfix(NAN))); + Require(remainder_nid_postfix(5.0, 3.0) == -1.0 && remainder_nid_postfix(7.0, 2.0) == -1.0 && remainder_nid_postfix(5.0, 2.0) == 1.0); + Require(std::signbit(remainder_nid_postfix(-4.0, 2.0)) && remainder_nid_postfix(1.0, INFINITY) == 1.0); + Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); @@ -385,6 +435,7 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) int main() { CheckFloatClassification(); CheckIntegerConversions(); + CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; const char input[] = "0x1.8p+2 remainder"; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b3fb88528..2fc9cff1a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -195,6 +195,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors - [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`, `truncl`, `logb` and `remainder` are exact operations and give the console's result - [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 - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified From 7663d8479cceb3d6fa7f1b9c10e88873966255f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 18:03:18 +0200 Subject: [PATCH 265/277] docs(technical-debt): note glibc errno for logb and remainder --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2fc9cff1a..c0f18c562 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -195,7 +195,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors - [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`, `truncl`, `logb` and `remainder` are exact operations and give the console's result +- [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 - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified From 3d8db7b5a8042a7f7e2d114b00f699094d05abc6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 10 Oct 2026 11:59:45 -0400 Subject: [PATCH 266/277] fix(json2): implement InitParameter2 base constructor Carry the implementation and regression from lucfx18's PR #2823, retaining the original Git author. Extend the regression with a leading guard and typed default-field checks, and record the unverified console layout. Source: https://github.com/boykopovar/AnyPS5/pull/2823 --- core/libs/prx/libSceJson2/Export.cpp | 5 ++--- core/libs/tests/GuestJson.cpp | 24 ++++++++++++++++++++---- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 0c68851f5..91717e619 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -838,9 +838,8 @@ int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { return 0; } -int APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { + _ZN3sce4Json14InitParameter2C1Ev(self); } int APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void) { diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 9d659d4c4..5df7b4369 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -88,6 +88,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value*, std::size_t) const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value*, const String*); Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value*, std::size_t); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(void*); +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void*); void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); @@ -370,16 +371,31 @@ static void ValueAccess() { alignas(16) std::uint8_t parameter[40]; std::memset(parameter, 0xff, sizeof(parameter)); _ZN3sce4Json14InitParameter2C1Ev(parameter); + alignas(16) std::uint8_t baseStorage[72]; + std::memset(baseStorage, 0xff, sizeof(baseStorage)); + auto* baseParameter = baseStorage + 16; + _ZN3sce4Json14InitParameter2C2Ev(baseParameter); + Require(std::memcmp(parameter, baseParameter, sizeof(parameter)) == 0); + void* defaultAllocator; + void* defaultUserData; + std::size_t defaultFileBufferSize; + std::memcpy(&defaultAllocator, baseParameter, sizeof(defaultAllocator)); + std::memcpy(&defaultUserData, baseParameter + sizeof(void*), sizeof(defaultUserData)); + std::memcpy(&defaultFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(defaultFileBufferSize)); + Require(defaultAllocator == nullptr && defaultUserData == nullptr && defaultFileBufferSize == 0); + for (std::size_t i = 0; i < 16; ++i) Require(baseStorage[i] == 0xff); + for (std::size_t i = 16 + 2 * sizeof(void*) + sizeof(std::size_t); i < sizeof(baseStorage); ++i) + Require(baseStorage[i] == 0xff); int allocator = 0; int userData = 0; - _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(parameter, &allocator, &userData); - _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 4096); + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(baseParameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(baseParameter, 4096); void* stored[3]{}; - std::memcpy(stored, parameter, sizeof(stored)); + std::memcpy(stored, baseParameter, sizeof(stored)); Require(stored[0] == &allocator && stored[1] == &userData && reinterpret_cast(stored[2]) == 4096); alignas(16) std::uint8_t initializer[16]{}; Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); - Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, baseParameter) == 0); Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b3fb88528..64fbbdf14 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -340,7 +340,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date -- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does +- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes. The base-object constructor forwards to the complete-object constructor; both zero these three fields, with console initialization bytes unverified. `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws From eaa9ce97ca599540089808b888851fb5b317b616 Mon Sep 17 00:00:00 2001 From: SibteProf Date: Sun, 11 Oct 2026 23:00:37 +0500 Subject: [PATCH 267/277] test(json2): isolate base constructor regression from float format changes --- core/libs/tests/GuestJson.cpp | 35 +++++++++++++++++++++++++++++------ docs/dev/TechnicalDebt.md | 3 ++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 5df7b4369..6b7309123 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -368,6 +368,24 @@ static void ValueAccess() { _ZN3sce4Json6StringD1Ev(&missing); _ZN3sce4Json5ValueD1Ev(&root); + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + _ZN3sce4Json14InitParameter2C1Ev(parameter); + int allocator = 0; + int userData = 0; + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(parameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 4096); + void* stored[3]{}; + std::memcpy(stored, parameter, sizeof(stored)); + Require(stored[0] == &allocator && stored[1] == &userData && reinterpret_cast(stored[2]) == 4096); + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); +} + +static void InitParameter2BaseConstructor() { alignas(16) std::uint8_t parameter[40]; std::memset(parameter, 0xff, sizeof(parameter)); _ZN3sce4Json14InitParameter2C1Ev(parameter); @@ -383,16 +401,20 @@ static void ValueAccess() { std::memcpy(&defaultUserData, baseParameter + sizeof(void*), sizeof(defaultUserData)); std::memcpy(&defaultFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(defaultFileBufferSize)); Require(defaultAllocator == nullptr && defaultUserData == nullptr && defaultFileBufferSize == 0); - for (std::size_t i = 0; i < 16; ++i) Require(baseStorage[i] == 0xff); - for (std::size_t i = 16 + 2 * sizeof(void*) + sizeof(std::size_t); i < sizeof(baseStorage); ++i) - Require(baseStorage[i] == 0xff); int allocator = 0; int userData = 0; _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(baseParameter, &allocator, &userData); _ZN3sce4Json14InitParameter217setFileBufferSizeEm(baseParameter, 4096); - void* stored[3]{}; - std::memcpy(stored, baseParameter, sizeof(stored)); - Require(stored[0] == &allocator && stored[1] == &userData && reinterpret_cast(stored[2]) == 4096); + void* storedAllocator; + void* storedUserData; + std::size_t storedFileBufferSize; + std::memcpy(&storedAllocator, baseParameter, sizeof(storedAllocator)); + std::memcpy(&storedUserData, baseParameter + sizeof(void*), sizeof(storedUserData)); + std::memcpy(&storedFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(storedFileBufferSize)); + Require(storedAllocator == &allocator && storedUserData == &userData && storedFileBufferSize == 4096); + for (std::size_t i = 0; i < 16; ++i) Require(baseStorage[i] == 0xff); + for (std::size_t i = 16 + sizeof(parameter); i < sizeof(baseStorage); ++i) + Require(baseStorage[i] == 0xff); alignas(16) std::uint8_t initializer[16]{}; Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, baseParameter) == 0); @@ -475,6 +497,7 @@ int main() { NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); + InitParameter2BaseConstructor(); ValueClear(); StreamParse(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 64fbbdf14..f0579a247 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -340,7 +340,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date -- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes. The base-object constructor forwards to the complete-object constructor; both zero these three fields, with console initialization bytes unverified. `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does +- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws @@ -618,6 +618,7 @@ Throughout the project, every function at every stage either **does exactly what - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [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. +- [sce::Json::InitParameter2 base-object constructor](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [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) - [_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 From 059e1319d15070342137a107b3f7506670d69ce4 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:57:16 +0200 Subject: [PATCH 268/277] feat(libkernel): export pause, sigwait and sigwaitinfo Exports pause (EDyhWelj7UY), sigwait (mrbHXqK8wkg) and sigwaitinfo (Uq5BGthgbl4) from libkernel. NIDs computed with tools/nid_names.py and matched in aerolib; ps5-payload-dev/sdk f5d40f02 sce_stubs/libkernel.c places all three in libkernel. They throw like the merged sigtimedwait (#3430), with the same message pattern: "sigwait: waiting for a pending signal", "sigwaitinfo: waiting for a pending signal" and "pause: waiting for a delivered signal". In FreeBSD 11.4 (8c639945) sigwaitinfo is kern_sigtimedwait without a timeout (sys/kern/kern_sig.c:1212), sigwait is the same wait returning the error number (kern_sig.c:1150, lib/libthr/thread/thr_sig.c:676-728) and pause is sigsuspend of the current mask (lib/libc/gen/pause.c:46-56). AnyPS5 never leaves a guest signal pending (blocked signals are dropped in Dispatch, raise delivers at once), so there is nothing to take, and raise, kill and pthread_kill run a handler only on the calling thread, so nothing can end a pause. Titles (predicted stops, registry order; not observed runs): - Ruffle PPSA68091, observed today "FAIL: unresolved ELF import 1t1-JoZ0sZQ from libSceLibcInternal.prx" (sinhf, #3519), predicted next "FAIL: unresolved ELF import EDyhWelj7UY from libkernel.prx" (pause, call site 0x8F7266). - RetroArch PPSA99169: pause (0x30DB1, an address table entry). - PS5MC PPSA99420: pause (0x3BD80F6, 0x3C9E220), sigwait (0x3C9E4C7), sigwaitinfo (0x3C9E588). - XPSemu PPSA97358: sigwait (0xE02AEC, 0x1101294, 0x1101368), QEMU's dummy vCPU thread and sigwait_compat thread. guest_sigtimedwait now also checks the three throws, with an empty set, a null info, a SIGINT raised while blocked, and that neither the output nor the blocked mask changes. TechnicalDebt records the call sites. Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libkernel/System/src/Signals.cpp | 19 ++++++ core/libs/tests/GuestSigtimedwait.cpp | 64 +++++++++++++++++-- docs/dev/TechnicalDebt.md | 1 + 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index f4a57b7ff..0e06f1538 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -288,6 +288,25 @@ int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, co NotImplemented_nid_no_patch("sigtimedwait: waiting for a pending signal"); return -1; } + +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { + (void)set; + (void)info; + NotImplemented_nid_no_patch("sigwaitinfo: waiting for a pending signal"); + return -1; +} + +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { + (void)set; + (void)received; + NotImplemented_nid_no_patch("sigwait: waiting for a pending signal"); + return -1; +} + +int APS5_VABI pause_nid_postfix(void) { + NotImplemented_nid_no_patch("pause: waiting for a delivered signal"); + return -1; +} } extern "C" { diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index d3b056aa7..83d93847a 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include struct GuestSignalSet { @@ -10,22 +11,71 @@ struct GuestTimespec { std::int64_t seconds; std::int64_t nanoseconds; }; -extern "C" int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +using Handler = void (APS5_VABI *)(int); +extern "C" { +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet*, void*); +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet*, int*); +int APS5_VABI pause_nid_postfix(void); +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI raise_nid_postfix(int); +int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +} +volatile int received = 0; +void APS5_VABI Callback(int value) { received = value; } static void Require(bool value) { if (!value) std::abort(); } -static bool ReportsNotImplemented(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { +template +static bool ReportsNotImplemented(TCall call, const char* message) { try { - sigtimedwait_nid_postfix(set, info, timeout); + call(); } catch (const std::runtime_error& error) { - return std::string(error.what()) == "sigtimedwait: waiting for a pending signal not implemented"; + return std::string(error.what()) == message; } return false; } +static bool SigtimedwaitReportsNotImplemented(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { + return ReportsNotImplemented([&] { sigtimedwait_nid_postfix(set, info, timeout); }, "sigtimedwait: waiting for a pending signal not implemented"); +} +static bool SigwaitinfoReportsNotImplemented(const GuestSignalSet* set, void* info) { + return ReportsNotImplemented([&] { sigwaitinfo_nid_postfix(set, info); }, "sigwaitinfo: waiting for a pending signal not implemented"); +} +static bool SigwaitReportsNotImplemented(const GuestSignalSet* set, int* taken) { + return ReportsNotImplemented([&] { sigwait_nid_postfix(set, taken); }, "sigwait: waiting for a pending signal not implemented"); +} int main() { const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; + const GuestSignalSet empty{{0, 0, 0, 0}}; + const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; unsigned char info[64] = {}; + const unsigned char untouched[64] = {}; const GuestTimespec poll{0, 0}; const GuestTimespec second{1, 0}; - Require(ReportsNotImplemented(&pipe, info, &poll)); - Require(ReportsNotImplemented(&pipe, nullptr, &second)); - Require(ReportsNotImplemented(&pipe, info, nullptr)); + Require(SigtimedwaitReportsNotImplemented(&pipe, info, &poll)); + Require(SigtimedwaitReportsNotImplemented(&pipe, nullptr, &second)); + Require(SigtimedwaitReportsNotImplemented(&pipe, info, nullptr)); + + Require(SigwaitinfoReportsNotImplemented(&pipe, info)); + Require(SigwaitinfoReportsNotImplemented(&pipe, nullptr)); + Require(SigwaitinfoReportsNotImplemented(&empty, info)); + Require(std::memcmp(info, untouched, sizeof(info)) == 0); + + int taken = -1; + Require(SigwaitReportsNotImplemented(&pipe, &taken)); + Require(SigwaitReportsNotImplemented(&empty, &taken)); + Require(taken == -1); + + Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); + + GuestSignalSet previous{}; + Require(signal_nid_postfix(2, Callback) != reinterpret_cast(static_cast(-1))); + Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(SigwaitReportsNotImplemented(&interrupt, &taken) && taken == -1); + Require(SigwaitinfoReportsNotImplemented(&interrupt, info)); + Require(std::memcmp(info, untouched, sizeof(info)) == 0); + GuestSignalSet current{}; + Require(_sigprocmask_nid_postfix(1, nullptr, ¤t) == 0 && current.bits[0] == (previous.bits[0] | interrupt.bits[0])); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + Require(raise_nid_postfix(2) == 0 && received == 2); + Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b3fb88528..033090d06 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -606,6 +606,7 @@ Throughout the project, every function at every stage either **does exactly what - [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`, and errno is preserved. `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. `openlog` and `closelog` are not implemented, so their identity and options are not applied - [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)) - [sigtimedwait](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` +- [pause, sigwait, sigwaitinfo](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throw, as `sigtimedwait` does: guest signals are never left pending, so `sigwait` and `sigwaitinfo` have nothing to take, and `pause` would wait for a handler that `raise`, `kill` and `pthread_kill` only run on the calling thread. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the throw at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)) - [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, 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 From db49377bef0f2663462d4df451c8c035a823b8b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:32:58 +0000 Subject: [PATCH 269/277] fix(libkernel): return SCE error codes from sceKernelFsync sceKernelFsync returned the host fsync or _commit result, so a failure returned -1 instead of an SCE error code, and a guest socket descriptor reached the host call. fsync then set the guest errno from the host errno rather than from that result. Handle sockets and return SCE error codes as sceKernelFdatasync does (EINVAL for an open socket, EBADF for a closed one or an unknown descriptor), and have fsync take the errno from sceKernelFsync's result like the other POSIX wrappers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- core/libs/prx/libkernel/File/src/Stdio.cpp | 9 +++++---- core/libs/tests/GuestFilesystem.cpp | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 51dfc6830..feac9dc15 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -725,11 +725,13 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { + if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - return NativeSync(fd); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else - return ::fsync(fd); + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #endif + return 0; } int APS5_VABI sceKernelFdatasync(int fd) { @@ -1131,8 +1133,7 @@ int APS5_VABI futimes_nid_postfix(int d, const KernelTimeval* times) { } int APS5_VABI fsync_nid_postfix(int fd) { - if (sceKernelFsync(fd) != 0) return PosixFailure(errno); - return 0; + return PosixResult(sceKernelFsync(fd)); } int APS5_VABI fdatasync_nid_postfix(int fd) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index acadafb4c..4dbe5fbb3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -30,6 +30,7 @@ int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI sceKernelFsync(int); int APS5_VABI fdatasync_nid_postfix(int); +int APS5_VABI fsync_nid_postfix(int); int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); @@ -358,6 +359,8 @@ int main() { Require(futimes_nid_postfix(descriptor, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(descriptor) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(descriptor) == static_cast(0x80020009u)); const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFchmod(socket, 0600) == static_cast(0x80020016u)); @@ -365,12 +368,17 @@ int main() { Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(socket) == static_cast(0x80020016u)); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(socket) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(socket) == static_cast(0x80020009u)); Require(sceKernelFdatasync(0x7fffffff) == static_cast(0x80020009u)); + Require(sceKernelFsync(0x7fffffff) == static_cast(0x80020009u)); Require(remove_nid_postfix(sized.string().c_str()) == 0); const auto present = root / "present.txt"; const auto presentName = present.string(); From 092f7ef00afa4b1e7c692ab41d9865d51fe82799 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:56:06 +0200 Subject: [PATCH 270/277] feat(libkernel): implement the pthread spinlock calls Add pthread_spin_init (ZMn3clnAGBA), pthread_spin_destroy (IJIggoPZExk), pthread_spin_lock (pw+70ClLYlY), pthread_spin_trylock (rCTGkBIHfPY) and pthread_spin_unlock (LEfMMCT+SlM). The NIDs were computed with tools/nid_names.py and match aerolib.csv. libkernel has no scePthreadSpin* twin in aerolib, so the calls are implemented directly, like the barrier. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import pthread_spin_lock. Predicted from relink registry order, not observed: after pthread_cond_reltimedwait_np, Porpoise stops with "FAIL: unresolved ELF import pw+70ClLYlY from libkernel.prx" (registry index 387; RetroArch index 493). No call site is resolved in the registry. Behaviour follows FreeBSD 11.4 libthr thr_pspinlock.c and thr_umtx.h (8c639945a44588f6cfdddfb057bb5aad88735da0). The guest pthread_spinlock_t holds a pointer to an allocated lock. EINVAL for a null or destroyed lock and for a pshared other than PTHREAD_PROCESS_PRIVATE or PTHREAD_PROCESS_SHARED, EBUSY from trylock while any thread holds the lock, EPERM from unlock by a thread that does not hold it. libthr spins forever when the holder locks again; that call throws here. A shared lock is allocated like a private one, since guest code runs in one process. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 13ebf7f926fa2ee1c825cb1fcf18451a8781c045) --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++ .../prx/libkernel/Pthread/Posix/Spinlock.cpp | 76 +++++++++++ core/libs/tests/GuestPthreadSpin.cpp | 122 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 210 insertions(+) create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp create mode 100644 core/libs/tests/GuestPthreadSpin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6fa23e5fe..c3e10c731 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp @@ -314,6 +315,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 000000000..3990f0d29 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int PROCESS_SHARED = 1; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; + if (!spinlock) return GUEST_ENOMEM; + *lock = spinlock; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 000000000..9783df2dd --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +static void ArgumentErrors() { + Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); + Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); + Require(lock == nullptr); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + SingleThread(PTHREAD_PROCESS_SHARED); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6fde01523..f659bc491 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -625,3 +625,4 @@ Throughout the project, every function at every stage either **does exactly what - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [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 +- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws From 774f21845dff20abddcdb02dabd4b9a6192c8ce3 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:58:03 +0200 Subject: [PATCH 271/277] feat(libkernel): implement pthread_cond_reltimedwait_np Add pthread_cond_reltimedwait_np (K953PF5u6Pc; computed with tools/nid_names.py and matching aerolib.csv) as scePthreadCondTimedwait with POSIX error numbers: ETIMEDOUT when the relative timeout expires, EPERM when the caller does not hold the mutex, and the mutex held again on return in every waiting case. Titles: Porpoise PPSA99764 and RetroArch PPSA99169 import it. Predicted from relink registry order, not observed: Porpoise stops with "FAIL: unresolved ELF import K953PF5u6Pc from libkernel.prx" (registry index 384; RetroArch index 490). No call site is resolved in the registry. The third argument is a relative timeout in microseconds, not a struct timespec. FreeBSD 11.4 libthr (8c639945a44588f6cfdddfb057bb5aad88735da0) has no such function. shadPS4 (3a9f57265d28975658f1ca941daa5c879c7a74bd, src/core/libraries/kernel/threads/condvar.cpp:229-234, 445, 467-468) registers posix_pthread_cond_reltimedwait_np(cond, mutex, u64 usec) for this NID and the same function for scePthreadCondTimedwait (BmMjYxmew1w), whose microsecond argument this repository already implements. The width follows the SCE twin's 32-bit SceKernelUseconds, as sem_reltimedwait_np does, not shadPS4's u64. There is no timespec to validate, so no EINVAL case. Recorded in TechnicalDebt. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit c86ec8417d12f5b12136b0f06eae06493b0e7a62) --- .../libs/prx/libkernel/Pthread/Posix/Cond.cpp | 4 ++ core/libs/tests/GuestCondTimedwait.cpp | 67 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 72 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211c..5c5e64d18 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -41,6 +41,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871..5a9190e1e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,12 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +27,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +46,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f659bc491..ab0e8c98a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,3 +626,4 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [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 - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [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 From 89351601607f8ca85f8288875e27f20a50fae85b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:23:14 +0000 Subject: [PATCH 272/277] fix(libkernel): handle available sem_timedwait counts first Co-authored-by: milescoviello --- core/libs/prx/libkernel/Pthread/Posix/Sem.cpp | 8 ++++ core/libs/tests/GuestPosixSem.cpp | 41 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 ++ 3 files changed, 52 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e71072e2a..d11bda07a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,14 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + { + std::lock_guard lock(semaphore->lock); + if (semaphore->count > 0) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1ae..f6a296312 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c699ad506..fccc07514 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -357,6 +357,9 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. + +- [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. + - [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 From 5164f7875a4c5b9e73f117402540c9096f884b27 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 09:42:03 +0200 Subject: [PATCH 273/277] feat(libSceVoiceQoS): implement sceVoiceQoSEnd End clears the init flag set by sceVoiceQoSInit and returns 0, so the library can be initialized again. Before an initialization it returns 0x804E0901 (LIBVOICEQOS_NOT_INIT), as measured on a PS5 (firmware 13.60) in #1407. LEGO Horizon Adventures (PPSA14632) calls it at shutdown and Ghost of Yotei (PPSA26344) calls it on its sceVoiceQoSInit failure branch. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 25fbc944faddc5fb3519c4ce4b8d3600b3bc9e1b) --- core/libs/prx/libSceVoiceQoS/Export.cpp | 7 +++++-- core/libs/tests/GuestVoiceQoS.cpp | 10 ++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4ab9fb411..4756143ee 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -9,6 +9,7 @@ namespace { constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; +constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); @@ -28,8 +29,10 @@ int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_ty } int APS5_VABI sceVoiceQoSEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + g_initialized = false; + return 0; } int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index bf8b3467c..44aa99634 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +int APS5_VABI sceVoiceQoSEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -13,8 +14,10 @@ int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); constexpr int initialized = static_cast(0x804E0905); + constexpr int notInitialized = static_cast(0x804E0901); const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); + Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -25,4 +28,11 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..fbb845c7a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -629,6 +629,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [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) +- [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_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 - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [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 From 6ce1240aba9d870cc2935015e6857d1783acadf8 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 11:07:42 +0200 Subject: [PATCH 274/277] feat(libSceContentExport): implement the export session calls sceContentExportStart hands out positive session ids, at most 10 live (SharpProspero MaxExecution), and sceContentExportFinish closes them. sceContentExportGetProgress, sceContentExportCancel and sceContentExportFromDataWithThumbnail take their Orbital signatures and check the session. The host has no media library to export into, so FromDataWithThumbnail throws before it calls either data callback, and GetProgress and Cancel throw for a live session since no export ever runs and their result for an idle session is unknown. An eleventh Start and Term with live sessions throw. Titles: Cult of the Lamb PPSA06465 imports GetProgress, Cancel and FromDataWithThumbnail (#2686); PPSA21564 (#271) and Cyberpunk 2077 PPSA04029 (#266) import Start. Sources: Orbital tools/ida/db_types.json (cc408367) for the signatures, SharpProspero Interop/Content/ContentExport.cs (5dfdb4b0) for the session model and MaxExecution, shadPS4 content_export.h (443f0224) for the parameter and callback layouts. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 473d6dd355d2c0ee22f9792927e0e465846569cf) --- core/libs/SceTypes.hpp | 13 +++++ .../prx/libSceContentExport/CMakeLists.txt | 2 +- core/libs/prx/libSceContentExport/Export.cpp | 52 ++++++++++++++----- core/libs/tests/GuestContentExport.cpp | 50 +++++++++++++++++- core/libs/tests/GuestContentUnimplemented.cpp | 6 --- docs/dev/TechnicalDebt.md | 3 +- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 51d48be50..36d3d47ec 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1590,6 +1590,19 @@ struct ContentExportInitParam2 { std::int64_t reserved1; }; +struct ContentExportParam { + char title[257]; + char comment[257]; + char content_type[65]; +}; +static_assert(sizeof(ContentExportParam) == 579); + +struct ContentExportCallbackParam { + std::size_t content_length; + void* func; + void* user_data; +}; + struct ContentSearchInitParam { std::size_t memory_size; }; struct PngEncCreateParam { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 7efbba37e..59e62f64b 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -15,7 +15,7 @@ if(BUILD_TESTING) add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentExport libSceContentSearch libSceContentDelete libc) + target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentSearch libSceContentDelete libc) configure_windows_unwind(guest_content_unimplemented_tests) add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) if(WIN32) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index a626f6a42..f618daeea 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,11 +3,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include #include #include namespace { +constexpr std::size_t MaxExecution = 10; std::atomic g_initialized{false}; +std::mutex g_sessionMutex; +std::set g_sessions; +std::int32_t g_lastSessionId = 0; + +void RequireSession(const char* function, std::int32_t expId) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); + if (!g_sessions.contains(expId)) throw std::invalid_argument(std::string(function) + ": unknown export session"); +} } extern "C" { @@ -21,9 +33,11 @@ int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { return 0; } -int APS5_VABI sceContentExportFinish(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFinish(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + g_sessions.erase(expId); + return 0; } int APS5_VABI sceContentExportFromData(void) { @@ -42,26 +56,36 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { } int APS5_VABI sceContentExportStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); + if (g_lastSessionId == std::numeric_limits::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); + g_sessions.insert(++g_lastSessionId); + return g_lastSessionId; } -int APS5_VABI sceContentExportGetProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); } -int APS5_VABI sceContentExportCancel(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportCancel(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); } -int APS5_VABI sceContentExportFromDataWithThumbnail(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); } int APS5_VABI sceContentExportTerm(void) { + std::lock_guard lock(g_sessionMutex); + if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); bool expected = true; if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 12038531a..ef295d6ec 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -1,25 +1,41 @@ #include "SceTypes.hpp" +#include #include +#include #include extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); +extern "C" int APS5_VABI sceContentExportStart(void); +extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { void Require(bool value) { if (!value) std::abort(); } -template +template bool Throws(TAction action) { try { action(); - } catch (const std::logic_error&) { + } catch (const TException&) { return true; } return false; } int allocator = 0; +char path[1024]{}; + +void Export(std::int32_t expId) { + ContentExportParam param{}; + ContentExportCallbackParam content{}; + ContentExportCallbackParam thumbnail{}; + sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} } @@ -27,6 +43,8 @@ int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); Require(Throws([] { sceContentExportTerm(); })); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -38,5 +56,33 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportTerm(); })); Require(sceContentExportInit2(¶m) == 0); + + std::set sessions; + for (int i = 0; i < 10; ++i) { + int session = sceContentExportStart(); + Require(session > 0); + Require(sessions.insert(session).second); + } + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportTerm(); })); + int first = *sessions.begin(); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(path[0] == 0); + Require(sceContentExportFinish(first) == 0); + Require(Throws([&] { sceContentExportFinish(first); })); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(Throws([] { sceContentExportFinish(0); })); + Require(Throws([] { sceContentExportFinish(-1); })); + sessions.erase(first); + int reopened = sceContentExportStart(); + Require(reopened > 0 && reopened != first); + Require(sessions.insert(reopened).second); + for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([&] { sceContentExportFinish(reopened); })); } diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp index 00a291f52..549946300 100644 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ b/core/libs/tests/GuestContentUnimplemented.cpp @@ -6,9 +6,6 @@ extern "C" { int APS5_VABI sceContentDeleteById(); -int APS5_VABI sceContentExportGetProgress(); -int APS5_VABI sceContentExportCancel(); -int APS5_VABI sceContentExportFromDataWithThumbnail(); int APS5_VABI sceContentSearchGetContentLastUpdateId(); int APS5_VABI sceContentSearchOpenMetadataByContentId(); int APS5_VABI sceContentSearchGetTotalContentSize(); @@ -34,9 +31,6 @@ bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { int main() { bool correct = true; correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - correct &= ReportsNotImplemented("sceContentExportGetProgress", sceContentExportGetProgress); - correct &= ReportsNotImplemented("sceContentExportCancel", sceContentExportCancel); - correct &= ReportsNotImplemented("sceContentExportFromDataWithThumbnail", sceContentExportFromDataWithThumbnail); correct &= ReportsNotImplemented("sceContentSearchGetContentLastUpdateId", sceContentSearchGetContentLastUpdateId); correct &= ReportsNotImplemented("sceContentSearchOpenMetadataByContentId", sceContentSearchOpenMetadataByContentId); correct &= ReportsNotImplemented("sceContentSearchGetTotalContentSize", sceContentSearchGetTotalContentSize); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..a794fbcd8 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -135,7 +135,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached -- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete), [sceContentExportGetProgress, sceContentExportCancel, sceContentExportFromDataWithThumbnail](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown - [sce::Json::Parser::parse with a read callback](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` @@ -408,6 +408,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [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 +- [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [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. - [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 From 3687b2e21477b53e59e6ce0401e3699b59c12373 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:49:37 +0400 Subject: [PATCH 275/277] perf(relinker): use a flat sorted call-site index --- core/relinker/CMakeLists.txt | 16 ++++++++ .../src/analysis/CallSiteResolver.cpp | 20 +++++++--- .../relinker/tests/CallSiteIndexTests.cpp | 37 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 core/relinker/relinker/tests/CallSiteIndexTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 095915729..4e8e1a4ad 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -161,6 +161,22 @@ if(BUILD_TESTING) add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + add_test_executable(call_site_index_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteIndexTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_index_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME call_site_index COMMAND call_site_index_tests) + set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 6f08ab26f..0c698568e 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -4,7 +4,8 @@ #include #include #include -#include +#include +#include namespace Relinker { @@ -70,7 +71,7 @@ public: std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; private: const std::vector* _cachedTextPtr = nullptr; - std::multimap _cachedTargetToInstr; + std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; }; @@ -82,6 +83,10 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector auto scanner = Codegen::MakeInstructionScanner(); auto instructions = scanner->ScanCodeSection(textSection, 0, textSection.size()); _cachedTargetToInstr.clear(); + const auto referenceCount = std::count_if(instructions.begin(), instructions.end(), [&](const auto& match) { + return isRipRelativeFF(textSection, match) || isRipRelativeMov64(textSection, match); + }); + _cachedTargetToInstr.reserve(static_cast(referenceCount)); for (const auto& m : instructions) { std::size_t dispOff = 0; if (isRipRelativeFF(textSection, m)) @@ -94,15 +99,20 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = static_cast(static_cast(instrEnd) + disp); - _cachedTargetToInstr.emplace(target, m.Offset); + _cachedTargetToInstr.emplace_back(target, m.Offset); } + std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; } std::vector sites; - auto lower = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress); - auto upper = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress + targetGotOrPltSize); + const auto findTarget = [&](VirtualAddress target) { + return std::lower_bound(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end(), + std::pair{target, 0}); + }; + auto lower = findTarget(targetGotOrPltAddress); + auto upper = findTarget(targetGotOrPltAddress + targetGotOrPltSize); for (auto it = lower; it != upper; ++it) sites.push_back(textSectionVAddr + it->second); return sites; diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp new file mode 100644 index 000000000..fceff1c26 --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Incorrect call-site index"); } + +int main() { + try { + std::vector code; + const std::vector targets{0x2000, 0x1000, 0x2000, 0x3000, 0x1008}; + for (const auto target : targets) { + const auto offset = code.size(); + code.insert(code.end(), {0xFF, 0x15, 0, 0, 0, 0}); + const auto disp = static_cast(target - (0x100 + offset + 6)); + std::memcpy(code.data() + offset + 2, &disp, 4); + } + auto resolver = Relinker::MakeCallSiteResolver(); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 8) == std::vector{0x106}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 9) == std::vector{0x106, 0x118}); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 0).empty()); + Require(resolver->ResolveCallSites(code, 0x100, 0x4000, 8).empty()); + auto copy = code; + Require(resolver->ResolveCallSites(copy, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(copy, 0x200, 0x2100, 8) == std::vector{0x200, 0x20C}); + std::vector mov{0x48, 0x8B, 0x05, 0xF9, 0x0F, 0, 0}; + Require(resolver->ResolveCallSites(mov, 0x1000, 0x2000, 8) == std::vector{0x1000}); + Require(resolver->ResolveCallSites({}, 0, 0, 1).empty()); + bool rejected = false; + 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"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} From 2b8f151b3f3d9e887ff1176440b1905dc3199c3a Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:09:51 -0400 Subject: [PATCH 276/277] test(agc): verify optimized pipeline draws and readback --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 18 +- .../Execution/include/VulkanDevice.hpp | 1 + .../execution/PipelineOptimizedReadback.cpp | 276 ++++++++++++++++++ 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e77f027d1..20c42fc92 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1038,7 +1038,7 @@ if(BUILD_TESTING) 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:$") 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_vector_window_tests agc_constant_bit_test_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 ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -1067,6 +1067,22 @@ if(BUILD_TESTING) set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 433582775..fe9ea5117 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -302,6 +302,7 @@ public: private: VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp new file mode 100644 index 000000000..e4e66f07f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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 namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 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} +}}; +constexpr std::array, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +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}; +} + +void CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.end()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + 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); + 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)} + }}; + State state{}; + state.stages = {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}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + OptimizationDrain drain{context.device}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 931aaa2a7e49b75d8ee28f2ba0625f4b36738dd5 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:09:22 +0530 Subject: [PATCH 277/277] feat(libkernel): handle sysctl queries for pathname, physmem and cacheline size --- .../System/src/SystemConfiguration.cpp | 70 +++++++++++++---- core/libs/tests/GuestSystemConfiguration.cpp | 77 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 + 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index abbd8d0ea..106e28393 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -16,6 +16,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -90,19 +91,46 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } - if (nameLength != 2 || name[0] != 6 || name[1] != 3) { - 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); + if (nameLength == 2 && name[0] == 6 && name[1] == 3) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t processors = sysconf_nid_postfix(58); + if (processors <= 0) return -1; + const int value = static_cast(processors); + return CopyOut(&value, sizeof(value), oldValue, oldLength); } - if (newValue != nullptr) { - *__error_nid_postfix() = 1; - return -1; + if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t pages = sysconf_nid_postfix(121); + if (pages <= 0) return -1; + const std::uint64_t physmem = static_cast(pages) * PS5_PAGE_SIZE; + return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } - const std::int64_t processors = sysconf_nid_postfix(58); - if (processors <= 0) return -1; - const int value = static_cast(processors); - return CopyOut(&value, sizeof(value), 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; + } + const int pid = name[3]; + if (pid != -1 && pid != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + static constexpr char path[] = "/app0/eboot.bin"; + return CopyOut(path, sizeof(path), oldValue, oldLength); + } + 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); } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { @@ -110,6 +138,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } + if (std::strcmp(name, "machdep.cacheline_size") == 0) { + *__error_nid_postfix() = 2; + return -1; + } + if (std::strcmp(name, "kern.proc.pathname") == 0) { + *__error_nid_postfix() = 22; + return -1; + } if (std::strcmp(name, "kern.rng_pseudo") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -121,9 +157,15 @@ 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, "hw.ncpu") != 0) throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); - static constexpr int mib[] = {6, 3}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + if (std::strcmp(name, "hw.ncpu") == 0) { + static constexpr int mib[] = {6, 3}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.physmem") == 0) { + static constexpr int mib[] = {6, 5}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index ffba40d37..e29adaa60 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -21,6 +21,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, const 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(); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -56,7 +57,7 @@ static void CheckProcessorCountSysctl() { } Require(threw); threw = false; - const int unknown[] = {6, 5}; + const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); } catch (const std::runtime_error&) { @@ -64,6 +65,77 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPhysicalMemorySysctl() { + const int mib[] = {6, 5}; + const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + 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(*__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(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); +} +static void CheckMachdepCachelineSizeSysctl() { + *__error_nid_postfix() = 13; + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + Require(value == 0x5a5a5a5a); +} +static void CheckPathnameSysctl() { + *__error_nid_postfix() = 13; + char buffer[32] = {}; + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 22); + + const int mibTooShort[] = {1, 14, 12}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 12, -1, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + 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")); + + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 12, callerPid}; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int mibForeign[] = {1, 14, 12, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + length = 5; + std::memset(buffer, 0xaa, sizeof(buffer)); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 5 && buffer[4] == '0' && buffer[5] == static_cast(0xaa)); + + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -109,6 +181,9 @@ static void CheckPseudoRandomSysctl() { } int main() { CheckProcessorCountSysctl(); + CheckPhysicalMemorySysctl(); + CheckMachdepCachelineSizeSysctl(); + CheckPathnameSysctl(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); *__error_nid_postfix() = 13; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a907d34b4..1522f23f7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -444,6 +444,8 @@ Throughout the project, every function at every stage either **does exactly what - [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [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. +- [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. - [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. - [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented.